using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;

/// <summary>
/// Code-behind for <c>InstanceConfigure.razor</c> — hosts the pure, side-effect-free
/// core of the CSV bulk-override import so it can be unit-pinned without
/// standing up the page's ≈7 injected services. The Razor side reads the uploaded
/// file's text, calls <see cref="OverrideCsvParser.Parse"/>, then feeds the result
/// here; on success it applies the returned dict through the SAME
/// <c>InstanceService.SetAttributeOverrideAsync</c> path the manual editor uses.
///
/// <para>The native-alarm-source CSV import (<see cref="BuildNativeAlarmSourceCsvImport"/>)
/// follows the identical shape: the Razor side parses the upload with the shared
/// <see cref="NativeAlarmSourceOverrideCsvParser"/> — the same parser the CLI
/// <c>instance native-alarm-source import --file</c> command uses — validates it here,
/// then upserts each row through the SAME repository path the inline editor's Save
/// uses.</para>
/// </summary>
public partial class InstanceConfigure
{
    /// <summary>
    /// Outcome of validating a parsed override CSV against an instance's overridable
    /// attributes. <see cref="Overrides"/> is the attribute-name → canonical-value-string
    /// map to apply (populated ONLY when there are no errors — all-or-nothing);
    /// <see cref="Errors"/> carries parser errors plus per-row validation errors, each
    /// pointing back at the operator's source line.
    /// </summary>
    internal sealed record CsvOverrideImportOutcome(
        IReadOnlyDictionary<string, string?> Overrides,
        IReadOnlyList<string> Errors)
    {
        /// <summary>True when at least one parser or validation error was collected.</summary>
        public bool HasErrors => Errors.Count > 0;
    }

    /// <summary>
    /// Validates a parsed override CSV against the instance's overridable attributes
    /// and, if everything checks out, builds the override dict to submit. Pure and
    /// deterministic — no I/O, no page state.
    ///
    /// <para>Rules (mirroring the manual editor):</para>
    /// <list type="bullet">
    ///   <item>The attribute name must exist in <paramref name="overridableAttributes"/>.
    ///   That set is the page's <c>_overrideAttrs</c> (already filtered to
    ///   non-template-locked rows), so a locked / unknown attribute is rejected
    ///   exactly as the editor would never offer it.</item>
    ///   <item>The value must be type-compatible with the attribute's
    ///   <c>DataType</c>/<c>ElementDataType</c>, validated by round-tripping through
    ///   <see cref="AttributeValueCodec"/> — the same codec the List editor uses.</item>
    ///   <item>A null value ("clear the override") is always accepted; downstream the
    ///   set-override path treats a null/empty value as a clear.</item>
    /// </list>
    ///
    /// <para>All-or-nothing: if ANY parser or validation error is present, the
    /// returned <see cref="CsvOverrideImportOutcome.Overrides"/> is empty so the
    /// caller applies nothing and surfaces the full error list.</para>
    /// </summary>
    /// <param name="parsed">The result of <see cref="OverrideCsvParser.Parse"/>.</param>
    /// <param name="overridableAttributes">The instance's non-locked attributes (the page's <c>_overrideAttrs</c>).</param>
    /// <returns>The validated override outcome; <see cref="CsvOverrideImportOutcome.Overrides"/> is
    /// empty and errors are populated when any row fails validation.</returns>
    internal static CsvOverrideImportOutcome BuildCsvOverrideImport(
        OverrideCsvParseResult parsed,
        IReadOnlyList<TemplateAttribute> overridableAttributes)
    {
        var errors = new List<string>(parsed.Errors);
        // Only non-template-locked attributes are overridable. The page already
        // passes its pre-filtered _overrideAttrs, but re-filter here so the rule is
        // self-contained and a locked attribute is rejected even if a caller passes
        // the raw template attribute list.
        var byName = overridableAttributes
            .Where(a => !a.IsLocked)
            .ToDictionary(a => a.Name, StringComparer.Ordinal);
        var overrides = new Dictionary<string, string?>(StringComparer.Ordinal);

        foreach (var row in parsed.Rows)
        {
            if (!byName.TryGetValue(row.AttributeName, out var attr))
            {
                errors.Add(
                    $"Line {row.LineNumber}: attribute '{row.AttributeName}' is not an "
                    + "overridable attribute on this instance (unknown or template-locked).");
                continue;
            }

            // A null value clears the override — always valid, no type check needed.
            if (row.Value is null)
            {
                overrides[row.AttributeName] = null;
                continue;
            }

            // Type compatibility: round-trip through the shared codec. For List
            // attributes the value is canonical JSON validated against the element
            // type fixed by the base attribute; for scalars we validate the literal
            // against the declared type. Mirrors the manual editor's guard.
            var error = ValidateValue(attr, row.Value, row.LineNumber);
            if (error is not null)
            {
                errors.Add(error);
                continue;
            }

            overrides[row.AttributeName] = row.Value;
        }

        // All-or-nothing: any error means nothing is applied.
        if (errors.Count > 0)
            return new CsvOverrideImportOutcome(
                new Dictionary<string, string?>(StringComparer.Ordinal), errors);

        return new CsvOverrideImportOutcome(overrides, Array.Empty<string>());
    }

    /// <summary>
    /// One validated native-alarm-source retarget from an imported CSV: the source
    /// binding's name plus the three optional override fields (null = keep the
    /// inherited value). A row whose three fields are ALL null means "no override" —
    /// the caller clears any existing override row for that source, exactly as the
    /// inline editor's Save does when every field is left blank.
    /// </summary>
    internal sealed record NativeAlarmSourceCsvImportRow(
        string SourceName, string? Connection, string? SourceReference, string? Filter)
    {
        /// <summary>True when all three override fields are blank (row means "inherit everything").</summary>
        public bool IsClear => Connection is null && SourceReference is null && Filter is null;
    }

    /// <summary>
    /// Outcome of validating a parsed native-alarm-source-override CSV against an
    /// instance's template source bindings. <see cref="Rows"/> is the ordered list of
    /// retargets to apply (populated ONLY when there are no errors — all-or-nothing);
    /// <see cref="Errors"/> carries parser errors plus per-row validation errors, each
    /// pointing back at the operator's source line.
    /// </summary>
    internal sealed record NativeAlarmSourceCsvImportOutcome(
        IReadOnlyList<NativeAlarmSourceCsvImportRow> Rows,
        IReadOnlyList<string> Errors)
    {
        /// <summary>True when at least one parser or validation error was collected.</summary>
        public bool HasErrors => Errors.Count > 0;
    }

    /// <summary>
    /// Validates a parsed native-alarm-source-override CSV against the template's
    /// source bindings and, if everything checks out, builds the retarget list to
    /// apply. Pure and deterministic — no I/O, no page state.
    ///
    /// <para>Rules — the same batch rules the CLI's
    /// <c>instance native-alarm-source import --file</c> path enforces server-side in
    /// <c>ManagementActor.HandleSetInstanceNativeAlarmSourceOverrides</c>:</para>
    /// <list type="bullet">
    ///   <item>The source name must resolve for this instance — an unknown name would
    ///   create a dead override the flattener silently drops.</item>
    ///   <item>The source must not be template-locked.</item>
    ///   <item>A source may appear at most once in the batch — a duplicate makes the
    ///   intended override ambiguous and the last-writer-wins upsert would silently
    ///   discard the earlier row.</item>
    /// </list>
    ///
    /// <para>Semantics are <b>merge, not full replace</b> (matching the CLI): sources
    /// absent from the CSV keep whatever override they already have, and a blank field
    /// within a row keeps the inherited value.</para>
    ///
    /// <para>All-or-nothing: if ANY parser or validation error is present, the returned
    /// <see cref="NativeAlarmSourceCsvImportOutcome.Rows"/> is empty so the caller
    /// applies nothing and surfaces the full error list.</para>
    /// </summary>
    /// <param name="parsed">The result of <see cref="NativeAlarmSourceOverrideCsvParser.Parse"/>.</param>
    /// <param name="templateSources">The template's native alarm source bindings (the page's <c>_nativeSources</c>).</param>
    /// <returns>The validated retarget outcome; <see cref="NativeAlarmSourceCsvImportOutcome.Rows"/>
    /// is empty and errors are populated when any row fails validation.</returns>
    internal static NativeAlarmSourceCsvImportOutcome BuildNativeAlarmSourceCsvImport(
        NativeAlarmSourceOverrideCsvParseResult parsed,
        IReadOnlyList<TemplateNativeAlarmSource> templateSources)
    {
        var errors = new List<string>(parsed.Errors);
        var byName = templateSources.ToDictionary(s => s.Name, StringComparer.Ordinal);
        var rows = new List<NativeAlarmSourceCsvImportRow>();
        var seen = new HashSet<string>(StringComparer.Ordinal);

        foreach (var row in parsed.Rows)
        {
            if (!byName.TryGetValue(row.SourceName, out var source))
            {
                errors.Add(
                    $"Line {row.LineNumber}: native alarm source '{row.SourceName}' does not "
                    + "resolve for this instance and cannot be overridden.");
                continue;
            }

            if (source.IsLocked)
            {
                errors.Add(
                    $"Line {row.LineNumber}: native alarm source '{row.SourceName}' is locked at "
                    + "the template level and cannot be overridden.");
                continue;
            }

            if (!seen.Add(row.SourceName))
            {
                errors.Add(
                    $"Line {row.LineNumber}: native alarm source '{row.SourceName}' appears more "
                    + "than once in the file.");
                continue;
            }

            rows.Add(new NativeAlarmSourceCsvImportRow(
                row.SourceName,
                Blank(row.Connection),
                Blank(row.SourceReference),
                Blank(row.Filter)));
        }

        // All-or-nothing: any error means nothing is applied.
        if (errors.Count > 0)
            return new NativeAlarmSourceCsvImportOutcome(Array.Empty<NativeAlarmSourceCsvImportRow>(), errors);

        return new NativeAlarmSourceCsvImportOutcome(rows, Array.Empty<string>());
    }

    /// <summary>
    /// Validates a single non-null override value against the attribute's declared
    /// type using <see cref="AttributeValueCodec"/>. Returns a line-qualified error
    /// string when incompatible, otherwise null.
    /// </summary>
    private static string? ValidateValue(TemplateAttribute attr, string value, int lineNumber)
    {
        try
        {
            if (attr.DataType == DataType.List)
            {
                // Canonical JSON array; Decode throws FormatException on malformed
                // JSON or an un-parseable element against the fixed element type.
                AttributeValueCodec.Decode(value, DataType.List, attr.ElementDataType ?? DataType.String);
            }
            else
            {
                // Scalar: validate the literal parses to the declared type. The codec
                // returns scalars unchanged, so we use the List machinery with a
                // single-element array to reuse its per-type parse rules.
                if (ScalarIsTypeChecked(attr.DataType))
                {
                    var probeJson = AttributeValueCodec.Encode(new List<string> { value });
                    AttributeValueCodec.Decode(probeJson, DataType.List, attr.DataType);
                }
            }
        }
        catch (FormatException ex)
        {
            return $"Line {lineNumber}: attribute '{attr.Name}' value is not a valid "
                 + $"{attr.DataType}{(attr.DataType == DataType.List ? $" of {attr.ElementDataType}" : string.Empty)} — {ex.Message}";
        }

        return null;
    }

    /// <summary>
    /// True for scalar types the codec can validate. String accepts any literal (no
    /// check); the remaining scalar types are parse-checked. Object/Binary aren't
    /// overridable scalars in this editor, so they pass through unchecked.
    /// </summary>
    private static bool ScalarIsTypeChecked(DataType dt) =>
        dt is DataType.Int32 or DataType.Float or DataType.Double
           or DataType.Boolean or DataType.DateTime;
}
