using System.Diagnostics;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using InnovEnergy.Lib.Protocols.DBus;
using InnovEnergy.Lib.Protocols.DBus.Daemon;
using InnovEnergy.Lib.Protocols.DBus.Protocol;
using InnovEnergy.Lib.Protocols.DBus.Protocol.DataTypes;
using InnovEnergy.Lib.Protocols.DBus.Protocol.Header;
using InnovEnergy.Lib.Utils;
using static System.Reactive.Linq.Observable;

namespace InnovEnergy.Lib.Victron.VeDBus;

public static class VePropertiesDbus
{
    
    // Task will run indefinitely unless an exception occurs
    public static async Task<Exception> PublishOnDBus(this VeProperties veProperties, Bus bus, String busName)
    {
        var con = new DBusConnection(bus);

        var calls = FromAsync(() => con.GetName<Message>(busName))
                   .IgnoreElements()
                   .Merge(con.IncomingMessages)
                   .Where(m => m.Type == MessageType.MethodCall)
                   .Select(msg => AnswerMethodCall(veProperties, msg))
                   .Do(con.OutgoingMessages.OnNext);
                   
        var changes = veProperties
                     .PropertyChanged
                     .Do(con.BroadcastPropertiesChanged);

        var initialPropertiesChanged = veProperties
                                      .ToObservable(TaskPoolScheduler.Default)
                                      .Do(con.BroadcastPropertiesChanged);
        
        return await calls
                    .MergeErrors(changes)
                    .MergeErrors(initialPropertiesChanged)
                    .Finally(con.Dispose);
    }

    public static async Task<Exception> PublishOnDBus(this VeProperties veProperties, DBusConnection con)
    {
        var calls = con.IncomingMessages
                       .Where(m => m.Type == MessageType.MethodCall)
                       .Select(msg => AnswerMethodCall(veProperties, msg))
                       .Do(con.OutgoingMessages.OnNext);
                   
        var changes = veProperties
                     .PropertyChanged
                     .Do(con.BroadcastPropertiesChanged);

        var initialPropertiesChanged = veProperties
                                      .ToObservable(TaskPoolScheduler.Default)
                                      .Do(con.BroadcastPropertiesChanged);
        
        return await calls
                    .MergeErrors(changes)
                    .MergeErrors(initialPropertiesChanged)
                    .Finally(con.Dispose);
    }

    private static async Task<T> GetName<T>(this DBusDaemonConnection dbusConnection, String busName)
    {
        var requestNameReply = await dbusConnection.RequestName(busName);

        if (requestNameReply != RequestNameReply.PrimaryOwner)
        {
            var error = $"Failed to reserve {busName} on DBus";
            Console.WriteLine(error);
            throw new Exception(error);
        }

        Console.WriteLine($"Successfully registered name {busName} on DBus " + dbusConnection.LocalName);
        
        return default!;
    }
    
    
    // TODO: SetValue
    private static Message AnswerMethodCall(VeProperties props, Message msg)
    {
        try
        {
            Debug.Write($"Got call from {msg.Sender} : {msg.Member} {msg.ObjectPath} => ");

            if (!msg.ObjectPath.HasValue)
                return msg.UnknownObjectPath("<none>");

            if (msg.ObjectPath == ObjectPath.Root)
            {
                return msg.Member switch
                {
                    VeDBusApi.GetValue => msg.Ok(props.GetValueOfAllProperties()),
                    VeDBusApi.GetText => msg.Ok(props.GetTextOfAllProperties()),
                    VeDBusApi.GetItems => msg.Ok(props.GetAllItems()),
                    _ => msg.UnknownMember(msg.Member)
                };
            }

            var prop = props.Get(msg.ObjectPath.Value);
            if (prop.HasValue)
            {
                var p = prop.Value;
                // Console.WriteLine(p.Writeable);
                return msg.Member switch
                {
                    VeDBusApi.GetValue => msg.Ok(p.Value),
                    VeDBusApi.GetText  => msg.Ok(p.Text),
                    VeDBusApi.GetItems => msg.Ok(p.GetItem()),
                    VeDBusApi.SetValue => p.Writeable && msg.ObjectPath != null
                                        ? msg.Ok(p.Value==msg.Payload! || props.Set(path: msg.ObjectPath.Value, value: msg.Payload!, writable: true) ? 0 : -1)
                                        : msg.ObjectNotWritable(msg.Member),
                    _ => msg.UnknownMember(msg.Member)
                };
            }

            return msg.UnknownObjectPath(msg.ObjectPath.Value);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            return msg.UnknownError(msg.ObjectPath.Value);
        }
    }

    private static Message Ok(this Message msg, Object payload)
    {
        Debug.WriteLine($"OK: {msg.ObjectPath} = {payload}");
        return msg.CreateMethodReturn(payload);
    }

    private static Message UnknownMember(this Message msg, String? member)
    {
        return CreateErrorReply(msg, $"Unknown Member: {member ?? "<none>"}");
    }

    private static Message UnknownError(this Message msg, ObjectPath objectPath)
    {
        return CreateErrorReply(msg, $"Unknown Error: {objectPath}");
    }
    private static Message UnknownObjectPath(this Message msg, ObjectPath objectPath)
    {
        return CreateErrorReply(msg, $"Unknown ObjectPath: {objectPath}");
    }
    
    private static Message ObjectNotWritable(this Message msg, ObjectPath objectPath)
    {
        return CreateErrorReply(msg, $"Not Writable Property: {objectPath}");
    }

    private static Message CreateErrorReply(Message msg, String error)
    {
        Debug.WriteLine("ERROR: " + error);
        return msg.CreateErrorReply(error);
    }
}