using System;
using UnityEngine;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Assets.Scripts;
using Assets.Scripts.Networks;
using Assets.Scripts.Objects.Electrical;
using Assets.Scripts.Objects.Motherboards;
using BepInEx;
using BepInEx.Configuration;
using Cysharp.Threading.Tasks;
using HarmonyLib;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using RemoteControl.Message;
using RemoteControl.Utils;
using GameDevice = Assets.Scripts.Objects.Pipes.Device;


namespace RemoteControl
{
    [BepInPlugin(pluginGuid, pluginName, pluginVersion)]
    [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
    public class RemoteControl : BepInEx.BaseUnityPlugin
    {
        // ReSharper disable once MemberCanBePrivate.Global

        // ReSharper disable once MemberCanBePrivate.Global
        public static ConfigEntry<int> Port;

        // ReSharper disable once MemberCanBePrivate.Global
        public static ConfigEntry<bool> ListenOnAllInterfaces;

        private readonly Harmony _harmony = new Harmony(pluginGuid);

        public static WebServer WebServer { get; private set; }
        internal static SubscriptionModule Subscribers { get; private set; }


        private CancellationTokenSource _modLifecycle = new();

        public const string pluginGuid = "com.thequux.stationeers.RemoteControl";
        public const string pluginName = "RemoteControl";
        public const string pluginVersion = "1.0";
        
        public static void Log(string line)
        {
            Debug.Log("[" + pluginName + "]: " + line);
        }

        private void StartRC()
        {
            if (!_modLifecycle.IsCancellationRequested)
            {
                _modLifecycle.Cancel();
            }
            
            _modLifecycle = new CancellationTokenSource();

            Port = Config.Bind(new ConfigDefinition("Server", "Port"), 39125,
                new ConfigDescription("The port to listen on"));
            ListenOnAllInterfaces = Config.Bind(
                new ConfigDefinition("Server", "ListenOnAllInterfaces"),
                false,
                new ConfigDescription("If set, listen on all interfaces. Otherwise, listen only on localhost"));


            var subscriptionModule = new SubscriptionModule("/subscribe");
            Subscribers = subscriptionModule;
            WebServer = new WebServer(o =>
                    o.WithUrlPrefix($"http://{(ListenOnAllInterfaces.Value ? "0.0.0.0" : "localhost")}:{Port.Value}/")
                        .WithEmbedIOHttpListener()
                )
                .WithWebApi("/api/v1", m => m.WithController<ApiController>())
                .WithModule(subscriptionModule);
            WebServer.Start(_modLifecycle.Token);

            _harmony.PatchAll();
            foreach (var patchedMethod in _harmony.GetPatchedMethods())
            {
                Log($"Patched {patchedMethod.FullDescription()}");
            }
            Log($"Patched {_harmony.GetPatchedMethods().Count()} methods total");

            
        }
        
        public void OnLoaded(List<GameObject> prefabs)
        {
            // Start();
#if DEVELOPMENT_BUILD
            Debug.Log($"Loaded {prefabs.Count} prefabs");
#endif
        }

        public void OnUnloaded(List<GameObject> prefabs)
        {
            // Stop();
        }

        public void StopRC()
        {
            _modLifecycle.Cancel();
            _harmony.UnpatchSelf();
        }

        public void Awake()
        {
            Log("Starting RemoteControl");
            StartRC();
        }

        private void OnDestroy()
        {
            Log("Tearing down RemoteControl");
            StopRC();
        }
    }

    internal class ApiController : WebApiController
    {
        private static readonly Dictionary<string, LogicType> LtByName = EnumCollections.LogicTypes.AsLookupDict(true);
        private static readonly Dictionary<string, LogicSlotType> StByName = EnumCollections.LogicSlotTypes.AsLookupDict(true);

        private static Device GetDeviceJson(GameDevice device) => new(device);
        
        
        private static Device? GetDeviceJson(long referenceId)
        {
            var dev = Referencable.Find<GameDevice>(referenceId);
            return dev == null ? null : new Device(dev);
        }

        private static GameDevice? GetNetworkDevice(string probeName, long referenceId)
        {
            GameDevice? dev = null;

            GameDevice.AllDevices.ForEach(anaDev =>
            {
                if (anaDev is CableAnalyser analyser)
                {
                    if (analyser.DisplayName != probeName) return false;
                    dev = analyser.CableNetwork.DeviceList.Find(netDev => netDev.ReferenceId == referenceId);
                    return dev != null;
                }

                return false;
            });

            return dev;
        }
        

        private static List<GameDevice> GetNetworkDevice(string probeName, string deviceName)
        {
            List<GameDevice> result = new();

            GameDevice.AllDevices.ForEach(anaDev =>
            {
                if (anaDev is not CableAnalyser analyser) return;
                if (analyser.DisplayName != probeName) return;
                var found = analyser.CableNetwork.DeviceList.Find(netDev => netDev.DisplayName == deviceName);
                if (found is not null)
                {
                    result.Add(found);
                }
            });

            return result;
        }

        private static T FindUniqueDeviceForHttp<T>(string networkName, string deviceName)
        {
            var result = default(T);
            bool isFound = false;

            GameDevice.AllDevices.ForEach(anaDev =>
            {
                if (anaDev is not CableAnalyser analyser) return;
                if (analyser.DisplayName != networkName) return;
                var found = analyser.CableNetwork.DeviceList.Find(netDev =>
                    netDev is T && (netDev.DisplayName == deviceName || netDev.ReferenceId.ToString() == deviceName));
                if (found is T item)
                {
                    if (isFound)
                    {
                        throw HttpException.InternalServerError("More than one object matches");
                    }

                    result = item;
                    isFound = true;
                }
            });
            if (!isFound)
            {
                throw HttpException.NotFound();
            }

            return result!;
        }
        
        [Route(HttpVerbs.Get, "/networks")]
        public Task<IList<string>> ListNetworks() => Task.FromResult(SubscriptionManager.GetProbes());

        [Route(HttpVerbs.Get, "/networks/{networkId}")]
        public Task<IDictionary<long, Device>> ListDevices(string networkId)
        {
            // lock (SubscriptionManager.Lock)
            using (new LogTimer("ListDevices"))
            {
                var networks = new HashSet<CableNetwork>();
                GameDevice.AllDevices.ForEach(dev =>
                {
                    if (dev is CableAnalyser analyser && dev.DisplayName == networkId)
                    {
                        networks.Add(analyser.CableNetwork);
                    }
                });

                var devices = new Dictionary<long, Device>();
                foreach (var network in networks)
                {
                    foreach (var device in network.DeviceList)
                    {
                        if (devices.ContainsKey(device.ReferenceId)) continue;
                        devices.Add(device.ReferenceId, GetDeviceJson(device));
                    }
                }
                return Task.FromResult<IDictionary<long, Device>>(devices);
            }
        }

        [Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/logic/{varId}")]
        public async Task SetLogic(string networkId, long deviceId, string varId)
        {
            LogicType ltype;
            if (!LtByName.TryGetValue(varId.ToLowerInvariant(), out ltype))
            {
                throw HttpException.NotFound();
            }

            var value = await HttpContext.GetRequestDataAsync<double>();
            var dev = GetNetworkDevice(networkId, deviceId) ?? throw HttpException.NotFound();
            if (dev.CanLogicWrite(ltype))
            {
                dev.SetLogicValue(ltype, value);
            }
            else
            {
                throw HttpException.MethodNotAllowed();
            }
        }

        [Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/slot/{slotId}/{varId}")]
        public async Task SetSlot(string networkId, string deviceId, int slotId, string varId)
        {
            LogicSlotType ltype;
            if (!StByName.TryGetValue(varId.ToLowerInvariant(), out ltype))
            {
                throw HttpException.NotFound();
            }

            var value = await HttpContext.GetRequestDataAsync<double>();
            var dev = FindUniqueDeviceForHttp<GameDevice>(networkId, deviceId);
            if (dev.CanLogicWrite(ltype, slotId))
            {
                dev.SetLogicValue(ltype, slotId, value);
            }
            else
            {
                throw HttpException.MethodNotAllowed();
            };
        }

        [Route(HttpVerbs.Patch, "/networks/{networkId}/device/{deviceId}")]
        public async Task PatchDevice(string networkId, string deviceId)
        {
            var values = await HttpContext.GetRequestDataAsync<Dictionary<string, double>>();
            if (values.Keys.Any(key => !LtByName.ContainsKey(key.ToLowerInvariant())))
            {
                throw HttpException.BadRequest();
            }

            var dev = FindUniqueDeviceForHttp<GameDevice>(networkId, deviceId);
            foreach (var keyValuePair in values)
            {
                var logicType = LtByName[keyValuePair.Key.ToLowerInvariant()];
                if (dev.CanLogicWrite(logicType))
                {
                    dev.SetLogicValue(logicType, keyValuePair.Value);
                }
            }
        }

        [Route(HttpVerbs.Get, "/networks/{networkId}/device/{deviceId}/code")]
        public UniTask<string> GetCode(string networkId, string deviceId)
        {
            var dev = FindUniqueDeviceForHttp<ICircuitHolder>(networkId, deviceId);
            return UniTask.FromResult(dev.GetSourceCode());
        }

        [Route(HttpVerbs.Post, "/networks/{networkId}/device/{deviceId}/code")]
        public UniTask<string> SetCode(string networkId, string deviceId, string code)
        {
            var dev = FindUniqueDeviceForHttp<ICircuitHolder>(networkId, deviceId);
            return UniTask.FromResult(dev.GetSourceCode());
        }
    }
}