using System.Collections.Immutable;
using System.Globalization;
using System.Net.Sockets;
using System.Reactive.Linq;
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using Google.Protobuf;
using InnovEnergy.Lib.Protocols.DBus;
using InnovEnergy.Lib.Protocols.DBus.Transport;
using InnovEnergy.Lib.Utils;
using InnovEnergy.Lib.Victron.VeDBus;
using InnovEnergy.VenusLogger.Parsers;
using InnovEnergy.WireFormat;
using InnovEnergy.WireFormat.VictronV1;
using CodedOutputStream = Google.Protobuf.CodedOutputStream;

namespace InnovEnergy.VenusLogger;

using Props    = ImmutableDictionary<String, VeProperty>;
using Services = IReadOnlyList<ServiceProperties>;

public static class Program
{
    const Int32 SamplePeriodSecs = 2;

    // rm -f $(pwd)/remote_dbus.sock ; ssh -nNT -L $(pwd)/remote_dbus.sock:/var/run/dbus/system_bus_socket root@10.2.1.6
    
    public static void Main()
    {
        Console.WriteLine("starting...");

        var serviceUrl = "https://sos-ch-dk-2.exo.io/";
        var keyName    = "EXO5ead8a3e6a908014025edc5c";
        var secret     = "xhCJ6gth-MS8ED4Qij3P7K12TfmkiqncCg70HxSGAe0";
        var bucket     = "graber-zurich";

        var dispatch = CreateS3Dispatcher(serviceUrl, keyName, secret, bucket, SamplePeriodSecs);

        var ep   = new UnixDomainSocketEndPoint("/home/eef/remote_dbus.sock");
        var auth = AuthenticationMethod.ExternalAsRoot();
        var bus  = new Bus(ep, auth);

        var dbus = new DBusConnection(bus);

        var batteries   = dbus.ObserveVeService(VeService.IsBatteryServiceName);
        var inverter    = dbus.ObserveVeService(VeService.IsInverterServiceName);
        var pvInverters = dbus.ObserveVeService(VeService.IsPvInverterServiceName);
        var mppts       = dbus.ObserveVeService(VeService.IsSolarChargerServiceName);
        var grid        = dbus.ObserveVeService(VeService.IsGridServiceName);
        var generator   = dbus.ObserveVeService(VeService.IsGeneratorServiceName);
        var settings    = dbus.ObserveVeService(VeService.IsSettingsServiceName);

        var services = Observable
                      .CombineLatest(batteries, inverter, pvInverters, mppts, grid, generator, settings)
                      .Select(EnumerableUtils.Flatten)
                      .Select(Enumerable.ToList)
                      .OfType<Services>();

        var interval = TimeSpan.FromSeconds(SamplePeriodSecs);

        Console.WriteLine("start sampling");

        services.Sample(interval)
                .Select(ParsePayload)
                .Skip(1)
                .SelectMany(dispatch)
                .Wait();
    }


    private static IObservable<IReadOnlyList<ServiceProperties>> ObserveVeService(this DBusConnection dbus,
                                                                                Func<String, Boolean> selector)
    {
        return dbus
              .ObservePropertiesOfServices(selector)
              .StartWith(Array.Empty<ServiceProperties>());
    }

    private static String GetInfo(PutObjectResponse response)
    {
        return DateTime
              .Now
              .ToUniversalTime()
              .ToString(CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern)
              .Replace('T', ' ')
              .Replace('-', '/') + "   " + response.HttpStatusCode;
    }

    private static Func<IMessage, Task<PutObjectResponse>> CreateS3Dispatcher(String serviceUrl,
                                                                              String keyName,
                                                                              String secret,
                                                                              String bucket,
                                                                               Int32 timeResolutionSecs = 2)
    {
        var buffer = new Byte[1024];

        var config = new AmazonS3Config
        {
            RegionEndpoint = RegionEndpoint.EUWest3,  // can be whatever
            ServiceURL     = serviceUrl
        };

        var s3Client = new AmazonS3Client(keyName, secret, config);

        return Dispatch;

        Task<PutObjectResponse> Dispatch(IMessage payload)
        {
            var now = DateTimeOffset.UtcNow;
            var timestamp = now.ToUnixTimeSeconds() / timeResolutionSecs * timeResolutionSecs;

            var request = new PutObjectRequest
            {
                BucketName      = bucket,
                Key             = timestamp.ToString(),
                ContentType     = "application/octet-stream",
                InputStream     = WriteToStream(payload),
                AutoCloseStream = false
            };

            return s3Client.PutObjectAsync(request);

            MemoryStream WriteToStream(IMessage data)
            {
                try
                {
                    var outputStream = new CodedOutputStream(buffer);
                    data.WriteTo(outputStream);
                    outputStream.Flush();
                    Console.WriteLine($"{now}: Writing {timestamp}: {outputStream.Position} bytes");
                    return new MemoryStream(buffer, 0, outputStream.Position.ConvertTo<Int32>());
                }
                catch (CodedOutputStream.OutOfSpaceException)
                {
                    buffer = new Byte[buffer.Length * 2];
                    return WriteToStream(data);
                }
            }
        }



    }


    public static Payload ParsePayload(Services services)
    {

        var grid      = services.GetGrid();
        var generator = services.GetGenerator();                  // TODO: generator
        var battery   = services.GetBattery();
        var pvOnDc    = services.GetPvCharger();
        var pvOnAcIn  = services.GetPvInverter(AcBusType.AcIn1);  // TODO: acIn2
        var pvOnAcOut = services.GetPvInverter(AcBusType.AcOut);
        var settings  = services.GetSettings();

        var (inverterAcIn, inverterAcOut, inverterDc) = services.GetInverterBusses();

        var acInBus  = CreateAcInBus(grid, pvOnAcIn, inverterAcIn);
        var acOutBus = CreateAcOutBus(settings, pvOnAcOut, inverterAcOut, acInBus);
        var dcBus    = CreateDcBus(settings, pvOnDc, inverterDc, battery);
        var losses   = CalcInverterLosses(acOutBus, dcBus);

        var inverter = new Device
        {
            Type    = DeviceType.Inverter,
            Devices = { acOutBus, dcBus, losses }
        };

        
        //Console.WriteLine(inverter);
        
        return new Payload { VictronTopologyV1 = new VictronTopologyV1 { Root = inverter } };
    }

    private static Device CalcInverterLosses(Maybe<Device> acBus, Maybe<Device> dcBus)
    {
        var acPower = acBus.SelectMany(ac => ac.Phases).Sum(p => p.Power);
        var dcPower = -dcBus.SelectMany(dc => dc.Phases).Sum(p => p.Power);

        //var losses = Math.Max(0, acPower - dcPower);
        var losses = dcPower - acPower;

        return new Device
        {
            Type = DeviceType.Losses,
            Phases = { new Phase { Power = losses } }
        };
    }

    private static String Debug(Maybe<Device> device)
    {
        var phases = device.SelectMany(d => d.Phases);

        var p = phases.Sum(p => p.Power) + "W   ";

        return phases
              .Select(p => " | " + p.Power.ToString().PadRight(6).Substring(0, 6) + "W")
              .Aggregate(p.PadRight(20) ,(a, b) => a + b);
    }

    private static Maybe<Device> CreateAcInBus(Maybe<Device> grid, Maybe<Device> pvOnAcIn, Maybe<Device> inverterAcIn)
    {
        if (!grid.HasValue && !pvOnAcIn.HasValue)
            return null;

        var loadOnAcIn = Devices
                        .EquivalentDevice(DeviceType.AcLoad, inverterAcIn, pvOnAcIn, grid)
                        .Select(Devices.ReversePhases);

        // loadOnAcIn.SelectMany(l => l.Phases)
        //           .ForEach(p =>
        //           {
        //               p.Current = Math.Min(0, p.Current);   // current in load cannot be positive
        //               p.Power   = Math.Min(0, p.Power);     // power of load cannot be positive
        //           });

        return Devices.Combine(DeviceType.AcInBus, loadOnAcIn, pvOnAcIn, grid);
    }

    private static Maybe<Device> CreateAcOutBus(VenusSettings settings, Maybe<Device> pvOnAcOut, Maybe<Device> inverterAcOut, Maybe<Device> acInBus)
    {
        if (!settings.HasAcOutBus || !pvOnAcOut.HasValue)
            return acInBus;

        var loadOnAcOut = Devices
                         .EquivalentDevice(DeviceType.AcLoad, inverterAcOut, pvOnAcOut)
                         .Select(Devices.ReversePhases);

        // loadOnAcOut.SelectMany(l => l.Phases)
        //            .ForEach(p =>
        //            {
        //                p.Current = Math.Min(0, p.Current);   // current in load cannot be positive
        //                p.Power   = Math.Min(0, p.Power);     // power of load cannot be positive
        //            });

        return Devices.Combine(DeviceType.AcOutBus, loadOnAcOut, pvOnAcOut, acInBus);
    }

    private static Maybe<Device> CreateDcBus(VenusSettings settings, Maybe<Device> pvOnDc, Maybe<Device> inverterDc, Maybe<Device> battery)
    {
        if (!pvOnDc.HasValue && !settings.HasDcSystem)
            return battery;

        // if (!settings.HasDcSystem)
        //     return Devices.Combine(DeviceType.DcBus, pvOnDc, battery);

        var loadOnDc = Devices
                      .EquivalentDevice(DeviceType.DcLoad, inverterDc, pvOnDc, battery)
//                      .Select(Devices.ReversePhases);
            ;

        // TODO: no way to measure battery heater power yet
        // if there is no DC-system declared:
        //      we assume that the missing (EquivalentDevice) power is consumed by the Battery Heater
        //
        // if there is a DC-system declared:
        //      lump the battery heater and the other devices into a DC load


        if (settings.HasDcSystem)
           return Devices.Combine(DeviceType.DcBus, loadOnDc, pvOnDc, battery);


        loadOnDc.ForEach(l => l.Type = DeviceType.BatteryHeater);
        battery.ForEach(b => b.Devices.Add(loadOnDc));
        return Devices.Combine(DeviceType.DcBus, pvOnDc, battery);
    }

}