using System.Text.Json;
using System.Text.Json.Serialization;
using ScadaLink.Commons.Types.DataConnections;

namespace ScadaLink.Commons.Serialization;

/// <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 for pre-refactor rows
/// and returns IsLegacy=true so the form can prompt the user to re-save.
/// </summary>
public static class OpcUaEndpointConfigSerializer
{
    private static readonly JsonSerializerOptions JsonOpts = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = false,
        Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
    };

    public static string Serialize(OpcUaEndpointConfig config)
        => JsonSerializer.Serialize(config, JsonOpts);

    public static (OpcUaEndpointConfig Config, bool IsLegacy) Deserialize(string? json)
    {
        if (string.IsNullOrWhiteSpace(json))
            return (new OpcUaEndpointConfig(), false);

        try
        {
            using var doc = JsonDocument.Parse(json);
            if (doc.RootElement.ValueKind == JsonValueKind.Object
                && doc.RootElement.TryGetProperty("endpointUrl", out _))
            {
                var typed = JsonSerializer.Deserialize<OpcUaEndpointConfig>(json, JsonOpts);
                if (typed != null)
                    return (typed, false);
            }
        }
        catch (JsonException) { /* fall through to legacy */ }

        try
        {
            return (LoadLegacy(json!), IsLegacy: true);
        }
        catch (JsonException)
        {
            return (new OpcUaEndpointConfig(), IsLegacy: true);
        }
    }

    /// <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>
    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,
            ["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;
    }

    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);

        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);
    }
}
