using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace TeamsISO.App.Services;

/// <summary>
/// Reads and writes NDI Access Manager's per-user config at
/// <c>%APPDATA%\NDI\ndi-config.v1.json</c>. This file controls global NDI behavior for
/// every NDI application on the machine — sender groups, receiver groups, RUDP/TCP
/// transport toggles, allowed adapters, etc. NDI applications read it on startup, so
/// changes here only take effect after restarting the affected app (Teams, OBS, etc.).
///
/// We use it to implement the "transcoder topology" requested by the user: pin Teams'
/// raw at-source-resolution NDI broadcasts to a private group (<c>teamsiso-input</c>) so
/// they don't pollute the production network, while TeamsISO's own clean normalized ISO
/// outputs continue to broadcast on the standard <c>Public</c> group that downstream
/// switchers and recorders default to.
///
/// The shape of ndi-config.v1.json is documented in the NDI 6 SDK headers; we work in
/// terms of <see cref="JsonNode"/> trees so we don't clobber unrelated keys (e.g. RUDP
/// settings the user may have customized in Access Manager).
/// </summary>
public static class NdiAccessManagerConfig
{
    /// <summary>
    /// Path to the NDI Access Manager config. <c>%APPDATA%\NDI\ndi-config.v1.json</c>.
    /// </summary>
    public static string ConfigPath =>
        Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            "NDI",
            "ndi-config.v1.json");

    /// <summary>
    /// Default name of the private group used for the transcoder topology.
    /// Matches the convention referenced in the NDI Network settings UI.
    /// </summary>
    public const string TranscoderInputGroup = "teamsiso-input";

    /// <summary>
    /// Result of an apply attempt. <see cref="Success"/> indicates the file was
    /// written or already had the desired groups. <see cref="BackupPath"/> is set
    /// to the path of the saved-aside copy of the prior config (when one existed),
    /// so the user can revert if they don't like the change.
    /// </summary>
    public sealed record ApplyResult(
        bool Success,
        string ConfigPath,
        string? BackupPath,
        string? ErrorMessage);

    /// <summary>
    /// Configures the machine-wide NDI groups so:
    /// <list type="bullet">
    ///   <item>All local senders (Teams, anything else) broadcast on
    ///         <paramref name="senderGroup"/> only — i.e. the private input group.</item>
    ///   <item>All local receivers see both <paramref name="senderGroup"/> and
    ///         <c>public</c> so TeamsISO can discover Teams' sources AND any
    ///         standard public sources from elsewhere on the network.</item>
    /// </list>
    /// TeamsISO's own per-pipeline <c>OutputGroups</c> still overrides the per-machine
    /// default at the sender level, so its normalized ISO outputs go on Public.
    /// </summary>
    /// <param name="senderGroup">Private group name for Teams' raw broadcasts.</param>
    public static ApplyResult ApplyTranscoderTopology(string senderGroup = TranscoderInputGroup)
    {
        try
        {
            var root = LoadOrCreate();
            var ndi = EnsureObject(root, "ndi");
            var groups = EnsureObject(ndi, "groups");
            groups["send"] = new JsonArray(senderGroup);
            groups["recv"] = new JsonArray("public", senderGroup);

            var backupPath = WriteWithBackup(root);
            return new ApplyResult(Success: true, ConfigPath: ConfigPath, BackupPath: backupPath, ErrorMessage: null);
        }
        catch (Exception ex)
        {
            return new ApplyResult(Success: false, ConfigPath: ConfigPath, BackupPath: null, ErrorMessage: ex.Message);
        }
    }

    /// <summary>
    /// Restores defaults: senders on <c>public</c>, receivers on <c>public</c>.
    /// Equivalent to undoing <see cref="ApplyTranscoderTopology"/>.
    /// </summary>
    public static ApplyResult RestoreDefaults()
    {
        try
        {
            var root = LoadOrCreate();
            var ndi = EnsureObject(root, "ndi");
            var groups = EnsureObject(ndi, "groups");
            groups["send"] = new JsonArray("public");
            groups["recv"] = new JsonArray("public");
            var backupPath = WriteWithBackup(root);
            return new ApplyResult(Success: true, ConfigPath: ConfigPath, BackupPath: backupPath, ErrorMessage: null);
        }
        catch (Exception ex)
        {
            return new ApplyResult(Success: false, ConfigPath: ConfigPath, BackupPath: null, ErrorMessage: ex.Message);
        }
    }

    /// <summary>
    /// Reads the current sender / receiver group lists, or null if the config doesn't
    /// exist yet (NDI Access Manager has never been opened on this machine).
    /// </summary>
    public static (IReadOnlyList<string>? Send, IReadOnlyList<string>? Recv) ReadCurrentGroups()
    {
        if (!File.Exists(ConfigPath)) return (null, null);
        try
        {
            using var stream = File.OpenRead(ConfigPath);
            var root = JsonNode.Parse(stream);
            var groups = root?["ndi"]?["groups"];
            return (
                AsStringList(groups?["send"]),
                AsStringList(groups?["recv"]));
        }
        catch
        {
            return (null, null);
        }
    }

    private static IReadOnlyList<string>? AsStringList(JsonNode? node) =>
        node is JsonArray arr ? arr.Select(n => n?.GetValue<string>() ?? string.Empty).ToArray() : null;

    private static JsonObject LoadOrCreate()
    {
        if (File.Exists(ConfigPath))
        {
            using var stream = File.OpenRead(ConfigPath);
            var existing = JsonNode.Parse(stream) as JsonObject;
            if (existing is not null) return existing;
        }
        return new JsonObject();
    }

    private static JsonObject EnsureObject(JsonNode? parent, string key)
    {
        if (parent is not JsonObject obj)
            throw new InvalidOperationException($"Cannot ensure key '{key}' on a non-object parent.");
        if (obj[key] is JsonObject existing) return existing;
        var fresh = new JsonObject();
        obj[key] = fresh;
        return fresh;
    }

    /// <summary>
    /// Writes the config atomically (temp file + replace) and saves a backup of the
    /// prior contents next to the original with a timestamp suffix. Returns the
    /// backup path if a prior file existed; null on first-write.
    /// </summary>
    private static string? WriteWithBackup(JsonNode root)
    {
        var dir = Path.GetDirectoryName(ConfigPath);
        if (!string.IsNullOrEmpty(dir))
            Directory.CreateDirectory(dir);

        string? backupPath = null;
        if (File.Exists(ConfigPath))
        {
            var stamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
            backupPath = ConfigPath + $".bak-{stamp}";
            File.Copy(ConfigPath, backupPath, overwrite: true);
        }

        var json = root.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
        var tempPath = ConfigPath + ".tmp";
        File.WriteAllText(tempPath, json);
        if (File.Exists(ConfigPath)) File.Replace(tempPath, ConfigPath, destinationBackupFileName: null);
        else File.Move(tempPath, ConfigPath);

        return backupPath;
    }
}
