using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;

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

/// <summary>
/// Outcome classification for <see cref="OpcUaEndpointConfigSerializer.Deserialize"/>.
/// </summary>
public enum OpcUaConfigParseStatus
{
    /// <summary>The stored JSON parsed cleanly as the current typed shape.</summary>
    Typed,

    /// <summary>
    /// The stored JSON parsed as the legacy flat string-dict shape. The returned
    /// config is usable; the caller may prompt the user to re-save in the new shape.
    /// </summary>
    Legacy,

    /// <summary>
    /// The stored JSON could not be parsed at all (genuinely malformed). The returned
    /// config is an empty default and the original string was lost — the caller should
    /// surface an error rather than presenting the empty config as the user's data.
    /// </summary>
    Malformed
}

/// <summary>
/// Result of <see cref="OpcUaEndpointConfigSerializer.Deserialize"/>. Carries the parsed
/// config plus an explicit <see cref="Status"/> distinguishing a recoverable legacy row
/// from genuinely unparseable input. Deconstructs into <c>(Config, IsLegacy)</c> for
/// backward compatibility with callers that only need those two values.
/// </summary>
public readonly record struct OpcUaConfigParseResult
{
    /// <summary>
    /// Initializes the result with the parsed config and its classification status.
    /// </summary>
    /// <param name="config">The parsed endpoint config (or an empty default on malformed input).</param>
    /// <param name="status">Classification of the parse outcome.</param>
    public OpcUaConfigParseResult(OpcUaEndpointConfig config, OpcUaConfigParseStatus status)
    {
        Config = config;
        Status = status;
    }

    /// <summary>The parsed config (an empty default when <see cref="Status"/> is Malformed).</summary>
    public OpcUaEndpointConfig Config { get; }

    /// <summary>Classification of the parse outcome.</summary>
    public OpcUaConfigParseStatus Status { get; }

    /// <summary>True when the source parsed as the legacy flat-dict shape.</summary>
    public bool IsLegacy => Status == OpcUaConfigParseStatus.Legacy;

    /// <summary>True when the source could not be parsed at all.</summary>
    public bool IsMalformed => Status == OpcUaConfigParseStatus.Malformed;

    /// <summary>
    /// Two-element deconstruction kept for backward compatibility. Note that
    /// <c>IsLegacy</c> is <c>false</c> for both <see cref="OpcUaConfigParseStatus.Typed"/>
    /// and <see cref="OpcUaConfigParseStatus.Malformed"/>; callers that need to tell those
    /// apart should read <see cref="Status"/> directly.
    /// </summary>
    /// <param name="config">Receives the parsed endpoint config.</param>
    /// <param name="isLegacy">Receives true when the source was the legacy flat-dict shape.</param>
    public void Deconstruct(out OpcUaEndpointConfig config, out bool isLegacy)
    {
        config = Config;
        isLegacy = IsLegacy;
    }
}

/// <summary>
/// Serializes <see cref="OpcUaEndpointConfig"/> to/from the typed nested JSON
/// shape stored in <c>DataConnection.PrimaryConfiguration</c> / <c>BackupConfiguration</c>.
/// On read, falls back to the legacy flat string-dict shape only for rows that are not
/// the current typed shape (no <c>endpointUrl</c> property), reporting
/// <see cref="OpcUaConfigParseStatus.Legacy"/> so the form can prompt the user to
/// re-save. A row that <em>is</em> the typed shape but fails to deserialize is reported
/// <see cref="OpcUaConfigParseStatus.Malformed"/>, never <see cref="OpcUaConfigParseStatus.Legacy"/>.
/// </summary>
public static class OpcUaEndpointConfigSerializer
{
    private static readonly JsonSerializerOptions JsonOpts = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = false,
        Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
    };

    /// <summary>
    /// Serializes an <see cref="OpcUaEndpointConfig"/> to the current typed JSON shape.
    /// </summary>
    /// <param name="config">The endpoint configuration to serialize.</param>
    /// <returns>A JSON string representing the configuration.</returns>
    public static string Serialize(OpcUaEndpointConfig config)
        => JsonSerializer.Serialize(config, JsonOpts);

    /// <summary>
    /// Parses stored OPC UA endpoint JSON. Tries the current typed shape first, then the
    /// legacy flat string-dict shape. The returned <see cref="OpcUaConfigParseResult.Status"/>
    /// distinguishes three outcomes:
    /// <list type="bullet">
    ///   <item><see cref="OpcUaConfigParseStatus.Typed"/> — clean parse of the current shape
    ///   (also returned for null/blank input, which yields a default config).</item>
    ///   <item><see cref="OpcUaConfigParseStatus.Legacy"/> — parsed as a legacy flat object;
    ///   the config is usable and the caller may prompt a re-save.</item>
    ///   <item><see cref="OpcUaConfigParseStatus.Malformed"/> — the input is genuinely
    ///   unparseable JSON, <em>or</em> it is the current typed shape (it has an
    ///   <c>endpointUrl</c> property) but typed deserialization failed — e.g. an
    ///   enum-valued field holding an unrecognised string or a wrong-typed field. Such a
    ///   corrupt typed row is reported <see cref="OpcUaConfigParseStatus.Malformed"/>
    ///   rather than being mislabelled <see cref="OpcUaConfigParseStatus.Legacy"/>, so the
    ///   offending field is not silently dropped. The config is an empty default and the
    ///   caller should surface an error rather than treating it as the user's saved data.</item>
    /// </list>
    /// </summary>
    /// <param name="json">The stored JSON string to parse; null or blank yields a default typed result.</param>
    /// <returns>An <see cref="OpcUaConfigParseResult"/> containing the parsed config and the detected parse status.</returns>
    public static OpcUaConfigParseResult Deserialize(string? json)
    {
        if (string.IsNullOrWhiteSpace(json))
            return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Typed);

        // First decide which shape the row is — without yet trying to materialize it.
        // A root JSON object carrying "endpointUrl" IS the current typed shape; anything
        // else (no endpointUrl) is treated as a candidate legacy flat-dict row.
        bool isTypedShape;
        try
        {
            using var doc = JsonDocument.Parse(json);
            isTypedShape = doc.RootElement.ValueKind == JsonValueKind.Object
                && doc.RootElement.TryGetProperty("endpointUrl", out _);
        }
        catch (JsonException)
        {
            // Could not even parse the document: genuinely malformed input.
            return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
        }

        if (isTypedShape)
        {
            // The row is the current typed shape. If typed deserialization fails the row
            // is a corrupt current-shape row (e.g. an invalid enum or wrong-typed field) —
            // it must NOT fall through to the legacy path and be mislabelled Legacy, which
            // would silently drop the offending field. Report Malformed instead.
            try
            {
                var typed = JsonSerializer.Deserialize<OpcUaEndpointConfig>(json, JsonOpts);
                if (typed != null)
                    return new OpcUaConfigParseResult(typed, OpcUaConfigParseStatus.Typed);
            }
            catch (JsonException) { /* corrupt typed row — classified Malformed below */ }

            return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
        }

        try
        {
            return new OpcUaConfigParseResult(LoadLegacy(json), OpcUaConfigParseStatus.Legacy);
        }
        catch (JsonException)
        {
            // Genuinely malformed input: not a recoverable legacy row. Report Malformed
            // (not Legacy) so the caller can surface an error instead of presenting an
            // empty config as if it were the user's saved configuration.
            return new OpcUaConfigParseResult(new OpcUaEndpointConfig(), OpcUaConfigParseStatus.Malformed);
        }
    }

    /// <summary>
    /// Flattens the typed config to the IDictionary&lt;string,string&gt; shape that
    /// IDataConnection.ConnectAsync expects. Keys match the historical convention
    /// used by OpcUaDataConnection so the adapter can keep that interface.
    /// </summary>
    /// <param name="config">The endpoint configuration to flatten.</param>
    /// <returns>A dictionary mapping connection-parameter key names to their string values.</returns>
    public static IDictionary<string, string> ToFlatDict(OpcUaEndpointConfig config)
    {
        var dict = new Dictionary<string, string>
        {
            ["endpoint"] = config.EndpointUrl,
            ["SecurityMode"] = config.SecurityMode.ToString(),
            ["AutoAcceptUntrustedCerts"] = config.AutoAcceptUntrustedCerts.ToString(),
            ["SessionTimeoutMs"] = config.SessionTimeoutMs.ToString(),
            ["OperationTimeoutMs"] = config.OperationTimeoutMs.ToString(),
            ["PublishingIntervalMs"] = config.PublishingIntervalMs.ToString(),
            ["SamplingIntervalMs"] = config.SamplingIntervalMs.ToString(),
            ["QueueSize"] = config.QueueSize.ToString(),
            ["KeepAliveCount"] = config.KeepAliveCount.ToString(),
            ["LifetimeCount"] = config.LifetimeCount.ToString(),
            ["MaxNotificationsPerPublish"] = config.MaxNotificationsPerPublish.ToString(),
            ["DiscardOldest"] = config.DiscardOldest.ToString(),
            ["SubscriptionPriority"] = config.SubscriptionPriority.ToString(),
            ["SubscriptionDisplayName"] = config.SubscriptionDisplayName,
            ["MaxMonitoredItemsPerSubscription"] = config.MaxMonitoredItemsPerSubscription.ToString(),
            ["TimestampsToReturn"] = config.TimestampsToReturn.ToString(),
        };
        if (config.Heartbeat is { } hb)
        {
            dict["HeartbeatTagPath"] = hb.TagPath;
            dict["HeartbeatMaxSilence"] = hb.MaxSilenceSeconds.ToString();
        }
        if (config.UserIdentity is { } ui)
        {
            dict["UserIdentity.TokenType"] = ui.TokenType.ToString();
            dict["UserIdentity.Username"] = ui.Username;
            dict["UserIdentity.Password"] = ui.Password;
            dict["UserIdentity.CertificatePath"] = ui.CertificatePath;
            dict["UserIdentity.CertificatePassword"] = ui.CertificatePassword;
        }
        if (config.Deadband is { } db)
        {
            dict["Deadband.Type"] = db.Type.ToString();
            dict["Deadband.Value"] = db.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
        }
        return dict;
    }

    /// <summary>
    /// Reconstructs an <see cref="OpcUaEndpointConfig"/> from the legacy flat string-dict shape.
    /// </summary>
    /// <param name="dict">The flat key-value dictionary produced by the legacy shape.</param>
    /// <returns>The reconstructed endpoint configuration.</returns>
    public static OpcUaEndpointConfig FromFlatDict(IDictionary<string, string> dict)
    {
        var c = new OpcUaEndpointConfig();

        if (dict.TryGetValue("endpoint", out var ep) && !string.IsNullOrWhiteSpace(ep))
            c.EndpointUrl = ep;
        else if (dict.TryGetValue("EndpointUrl", out var ep2) && !string.IsNullOrWhiteSpace(ep2))
            c.EndpointUrl = ep2;

        if (dict.TryGetValue("SecurityMode", out var smStr)
            && Enum.TryParse<OpcUaSecurityMode>(smStr, ignoreCase: true, out var sm))
            c.SecurityMode = sm;

        if (dict.TryGetValue("AutoAcceptUntrustedCerts", out var aacStr)
            && bool.TryParse(aacStr, out var aac))
            c.AutoAcceptUntrustedCerts = aac;

        TryAssignInt(dict, "SessionTimeoutMs", v => c.SessionTimeoutMs = v);
        TryAssignInt(dict, "OperationTimeoutMs", v => c.OperationTimeoutMs = v);
        TryAssignInt(dict, "PublishingIntervalMs", v => c.PublishingIntervalMs = v);
        TryAssignInt(dict, "SamplingIntervalMs", v => c.SamplingIntervalMs = v);
        TryAssignInt(dict, "QueueSize", v => c.QueueSize = v);
        TryAssignInt(dict, "KeepAliveCount", v => c.KeepAliveCount = v);
        TryAssignInt(dict, "LifetimeCount", v => c.LifetimeCount = v);
        TryAssignInt(dict, "MaxNotificationsPerPublish", v => c.MaxNotificationsPerPublish = v);
        TryAssignInt(dict, "MaxMonitoredItemsPerSubscription", v => c.MaxMonitoredItemsPerSubscription = v);

        if (dict.TryGetValue("DiscardOldest", out var doStr) && bool.TryParse(doStr, out var doVal))
            c.DiscardOldest = doVal;
        if (dict.TryGetValue("SubscriptionPriority", out var spStr) && byte.TryParse(spStr, out var spVal))
            c.SubscriptionPriority = spVal;
        if (dict.TryGetValue("SubscriptionDisplayName", out var sdnStr) && !string.IsNullOrWhiteSpace(sdnStr))
            c.SubscriptionDisplayName = sdnStr;
        if (dict.TryGetValue("TimestampsToReturn", out var ttrStr)
            && Enum.TryParse<OpcUaTimestampsToReturn>(ttrStr, ignoreCase: true, out var ttr))
            c.TimestampsToReturn = ttr;

        if (dict.TryGetValue("HeartbeatTagPath", out var hbPath)
            && !string.IsNullOrWhiteSpace(hbPath))
        {
            var hb = new OpcUaHeartbeatConfig { TagPath = hbPath };
            TryAssignInt(dict, "HeartbeatMaxSilence", v => hb.MaxSilenceSeconds = v);
            c.Heartbeat = hb;
        }

        if (dict.TryGetValue("UserIdentity.TokenType", out var uiTt)
            && Enum.TryParse<OpcUaUserTokenType>(uiTt, ignoreCase: true, out var tokenType))
        {
            var ui = new OpcUaUserIdentityConfig { TokenType = tokenType };
            if (dict.TryGetValue("UserIdentity.Username", out var u)) ui.Username = u;
            if (dict.TryGetValue("UserIdentity.Password", out var p)) ui.Password = p;
            if (dict.TryGetValue("UserIdentity.CertificatePath", out var cp)) ui.CertificatePath = cp;
            if (dict.TryGetValue("UserIdentity.CertificatePassword", out var cpw)) ui.CertificatePassword = cpw;
            c.UserIdentity = ui;
        }

        if (dict.TryGetValue("Deadband.Type", out var dbT)
            && Enum.TryParse<OpcUaDeadbandType>(dbT, ignoreCase: true, out var dbType))
        {
            var db = new OpcUaDeadbandConfig { Type = dbType };
            if (dict.TryGetValue("Deadband.Value", out var dbV)
                && double.TryParse(dbV, System.Globalization.NumberStyles.Float,
                    System.Globalization.CultureInfo.InvariantCulture, out var dbVal))
                db.Value = dbVal;
            c.Deadband = db;
        }

        return c;
    }

    private static OpcUaEndpointConfig LoadLegacy(string json)
    {
        using var doc = JsonDocument.Parse(json);
        if (doc.RootElement.ValueKind != JsonValueKind.Object)
            throw new JsonException("Legacy JSON must be a flat object.");

        var dict = new Dictionary<string, string>();
        foreach (var prop in doc.RootElement.EnumerateObject())
        {
            // JsonElement.ToString() returns the raw value for primitives:
            // "1000" for numbers, "true"/"false" for booleans, the string content
            // for strings. Nested objects/arrays serialize to their JSON; we don't
            // expect those in the legacy flat-dict shape, but FromFlatDict will
            // simply ignore unknown keys.
            dict[prop.Name] = prop.Value.ValueKind switch
            {
                JsonValueKind.String => prop.Value.GetString() ?? "",
                JsonValueKind.Number => prop.Value.GetRawText(),
                JsonValueKind.True => "True",
                JsonValueKind.False => "False",
                _ => prop.Value.ToString()
            };
        }
        return FromFlatDict(dict);
    }

    private static void TryAssignInt(IDictionary<string, string> dict, string key, Action<int> assign)
    {
        if (dict.TryGetValue(key, out var s) && int.TryParse(s, out var v))
            assign(v);
    }
}
