using System.Reflection;
using NewLife;
using NewLife.Caching;
using NewLife.Log;
using NewLife.Remoting;
using NewLife.Security;
using NewLife.Serialization;
using NewLife.Web;
using Stardust.Data.Nodes;
using Stardust.Models;
using Stardust.Server.Models;
using XCode;

namespace Stardust.Server.Services;

public class NodeService
{
    private static readonly ICache _cache = new MemoryCache();
    private readonly TokenService _tokenService;
    private readonly ICache _queue;
    private readonly ITracer _tracer;

    public NodeService(TokenService tokenService, ICache queue, ITracer tracer)
    {
        _tokenService = tokenService;
        _queue = queue;
        _tracer = tracer;
    }

    #region 注册&登录
    public Boolean Auth(Node node, String secret, LoginInfo inf, String ip)
    {
        if (node == null) return false;

        node = CheckNode(node, inf.Node, inf.ProductCode, ip);
        if (node == null) return false;

        if (node.Secret.IsNullOrEmpty()) return true;
        return !secret.IsNullOrEmpty() && !secret.IsNullOrEmpty() && (node.Secret == secret || node.Secret.MD5() == secret);
    }

    public Node Register(LoginInfo inf, String ip, StarServerSetting setting)
    {
        var code = inf.Code;
        var secret = inf.Secret;

        var node = Node.FindByCode(code, true);

        // 校验唯一编码，防止客户端拷贝配置
        if (node == null)
        {
            node = AutoRegister(null, inf, ip, setting);
        }
        else
        {
            // 登录密码未设置或者未提交，则执行动态注册
            if (node == null || node.Secret.IsNullOrEmpty() || secret.IsNullOrEmpty())
                node = AutoRegister(node, inf, ip, setting);
            else if (node.Secret.MD5() != secret)
                node = AutoRegister(node, inf, ip, setting);
        }

        return node;
    }

    public TokenModel Login(Node node, LoginInfo inf, String ip, StarServerSetting setting)
    {
        if (!inf.ProductCode.IsNullOrEmpty()) node.ProductCode = inf.ProductCode;

        node.UpdateIP = ip;
        node.FixNameByRule();
        node.Login(inf.Node, ip);

        // 设置令牌
        var tokenModel = IssueToken(node.Code, setting);

        // 在线记录
        var olt = GetOrAddOnline(node, tokenModel.AccessToken, ip);
        olt.Save(inf.Node, null, tokenModel.AccessToken, ip);

        // 登录历史
        node.WriteHistory("节点鉴权", true, $"[{node.Name}/{node.Code}]鉴权成功 " + inf.ToJson(false, false, false), ip);

        return tokenModel;
    }

    /// <summary>注销</summary>
    /// <param name="reason">注销原因</param>
    /// <param name="ip">IP地址</param>
    /// <returns></returns>
    public NodeOnline Logout(Node node, String reason, String ip)
    {
        var online = GetOnline(node, ip);
        if (online == null) return null;

        var msg = $"{reason} [{node}]]登录于{online.CreateTime}，最后活跃于{online.UpdateTime}";
        node.WriteHistory("节点下线", true, msg, ip);
        online.Delete();

        //var sid = $"{node.ID}@{ip}";
        var sid = node.Code;
        _cache.Remove($"NodeOnline:{sid}");

        // 计算在线时长
        if (online.CreateTime.Year > 2000)
        {
            node.OnlineTime += (Int32)(DateTime.Now - online.CreateTime).TotalSeconds;
            node.SaveAsync();
        }

        NodeOnlineService.CheckOffline(node, "注销");

        return online;
    }

    /// <summary>
    /// 校验节点密钥
    /// </summary>
    /// <param name="node"></param>
    /// <param name="ps"></param>
    /// <returns></returns>
    private static Node CheckNode(Node node, NodeInfo di, String productCode, String ip)
    {
        // 校验唯一编码，防止客户端拷贝配置
        var uuid = di.UUID;
        var guid = di.MachineGuid;
        var serial = di.SerialNumber;
        var board = di.Board;
        var diskid = di.DiskID;

        var flag = true;
        if (!uuid.IsNullOrEmpty() && uuid != node.Uuid)
        {
            node.WriteHistory("登录校验", false, $"唯一标识不符！{uuid}!={node.Uuid}", ip);
            flag = false;
        }
        if (!guid.IsNullOrEmpty() && guid != node.MachineGuid)
        {
            node.WriteHistory("登录校验", false, $"机器标识不符！{guid}!={node.MachineGuid}", ip);
            flag = false;
        }
        if (!serial.IsNullOrEmpty() && serial != node.SerialNumber)
        {
            node.WriteHistory("登录校验", false, $"计算机序列号不符！{serial}!={node.SerialNumber}", ip);
            flag = false;
        }
        if (!board.IsNullOrEmpty() && board != node.Board)
        {
            node.WriteHistory("登录校验", false, $"主板不符！{board}!={node.Board}", ip);
            //flag = false;
        }
        if (!diskid.IsNullOrEmpty() && diskid != node.DiskID)
        {
            node.WriteHistory("登录校验", false, $"磁盘序列号不符！{diskid}!={node.DiskID}", ip);
            flag = false;
        }
        if (!node.ProductCode.IsNullOrEmpty() && !productCode.IsNullOrEmpty() && !node.ProductCode.EqualIgnoreCase(productCode))
        {
            node.WriteHistory("登录校验", false, $"产品编码不符！{productCode}!={node.ProductCode}", ip);
            flag = false;
        }

        // 机器名
        if (di.MachineName != node.MachineName)
        {
            node.WriteHistory("登录校验", false, $"机器名不符！{di.MachineName}!={node.MachineName}", ip);
        }

        // 网卡地址
        if (di.Macs != node.MACs)
        {
            var dims = di.Macs?.Split(",") ?? new String[0];
            var nodems = node.MACs?.Split(",") ?? new String[0];
            // 任意网卡匹配则通过
            if (!nodems.Any(e => dims.Contains(e)))
            {
                node.WriteHistory("登录校验", false, $"网卡地址不符！{di.Macs}!={node.MACs}", ip);
            }
        }

        return flag ? node : null;
    }

    private Node AutoRegister(Node node, LoginInfo inf, String ip, StarServerSetting set)
    {
        if (!set.AutoRegister) throw new ApiException(12, "禁止自动注册");

        // 检查白名单
        //var ip = UserHost;
        if (!IsMatchWhiteIP(set.WhiteIP, ip)) throw new ApiException(13, "非法来源，禁止注册");

        var di = inf.Node;
        var code = BuildCode(di, inf.ProductCode, set);
        if (code.IsNullOrEmpty()) code = Rand.NextString(8);

        // 如果节点编码有改变，则倾向于新建节点
        if (node == null || node.Code != code) node = Node.FindByCode(code);

        if (node == null)
        {
            // 该硬件的所有节点信息
            var list = Node.Search(di.UUID, di.MachineGuid, di.Macs);

            // 当前节点信息，取较老者
            list = list.Where(e => e.ProductCode.IsNullOrEmpty() || e.ProductCode == inf.ProductCode).OrderBy(e => e.ID).ToList();

            // 找到节点
            node ??= list.FirstOrDefault();
        }

        var name = "";
        if (name.IsNullOrEmpty()) name = di.MachineName;
        if (name.IsNullOrEmpty()) name = di.UserName;

        node ??= new Node
        {
            Enable = true,

            CreateIP = ip,
            CreateTime = DateTime.Now,
        };

        // 如果未打开动态注册，则把节点修改为禁用
        node.Enable = set.AutoRegister;

        if (node.Name.IsNullOrEmpty()) node.Name = name;

        // 优先使用节点散列来生成节点证书，确保节点路由到其它接入网关时保持相同证书代码
        node.Code = code;

        node.Secret = Rand.NextString(16);
        node.UpdateIP = ip;
        node.UpdateTime = DateTime.Now;

        node.Save();
        //autoReg = true;

        // 第一个注册的StarAgent提高采样频率，便于测试和演示
        if (node.ID == 1)
        {
            node.Period = 5;
            node.Update();
        }

        node.WriteHistory("动态注册", true, inf.ToJson(false, false, false), ip);

        return node;
    }

    /// <summary>
    /// 是否匹配白名单，未设置则直接通过
    /// </summary>
    /// <param name="whiteIp"></param>
    /// <param name="ip"></param>
    /// <returns></returns>
    private static Boolean IsMatchWhiteIP(String whiteIp, String ip)
    {
        if (ip.IsNullOrEmpty()) return true;
        if (whiteIp.IsNullOrEmpty()) return true;

        var ss = whiteIp.Split(",");
        foreach (var item in ss)
        {
            if (item.IsMatch(ip)) return true;
        }

        return false;
    }

    private static String BuildCode(NodeInfo di, String productCode, StarServerSetting set)
    {
        //var set = Setting.Current;
        //var uid = $"{di.UUID}@{di.MachineGuid}@{di.Macs}";
        var ss = (set.NodeCodeFormula + "").Split('(', ')');
        if (ss.Length >= 2)
        {
            var uid = ss[1];
            uid = uid.Replace("{ProductCode}", productCode);
            foreach (var pi in di.GetType().GetProperties())
            {
                uid = uid.Replace($"{{{pi.Name}}}", pi.GetValue(di) + "");
            }
            if (uid.Contains('{') || uid.Contains('}')) XTrace.WriteLine("节点编码公式有误，存在未解析变量，uid={0}", uid);
            if (!uid.IsNullOrEmpty())
            {
                XTrace.WriteLine("生成节点编码 uid={0} alg={1}", uid, ss[0]);

                // 使用产品类别加密一下，确保不同类别有不同编码
                var buf = uid.GetBytes();
                //code = buf.Crc().GetBytes().ToHex();
                switch (ss[0].ToLower())
                {
                    case "crc": buf = buf.Crc().GetBytes(); break;
                    case "crc16": buf = buf.Crc16().GetBytes(); break;
                    case "md5": buf = buf.MD5(); break;
                    case "md5_16": buf = uid.MD5_16().ToHex(); break;
                    default:
                        break;
                }
                return buf.ToHex();
            }
        }

        return null;
    }
    #endregion

    #region 心跳
    public PingResponse Ping(Node node, PingInfo inf, String token, String ip, StarServerSetting set)
    {
        var rs = new PingResponse
        {
            Time = inf.Time,
            ServerTime = DateTime.UtcNow,
        };

        if (node != null)
        {
            if (!inf.IP.IsNullOrEmpty()) node.IP = inf.IP;
            node.UpdateIP = ip;
            node.FixArea();
            node.FixNameByRule();

            // 每10分钟更新一次节点信息，确保活跃
            if (node.LastActive.AddMinutes(10) < DateTime.Now) node.LastActive = DateTime.Now;
            node.SaveAsync();

            rs.Period = node.Period;

            var olt = GetOrAddOnline(node, token, ip);
            olt.Name = node.Name;
            olt.Category = node.Category;
            olt.Version = node.Version;
            olt.CompileTime = node.CompileTime;
            olt.OSKind = node.OSKind;
            olt.Save(null, inf, token, ip);

            // 令牌有效期检查，10分钟内到期的令牌，颁发新令牌。
            //todo 这里将来由客户端提交刷新令牌，才能颁发新的访问令牌。
            var tm = ValidAndIssueToken(node.Code, token, set);
            if (tm != null)
            {
                rs.Token = tm.AccessToken;

                //node.WriteHistory("刷新令牌", true, tm.ToJson(), ip);
            }

            if (!node.Version.IsNullOrEmpty() && Version.TryParse(node.Version, out var ver))
            {
                // 拉取命令
                if (ver.Build >= 2003 && ver.Revision >= 107)
                    rs.Commands = AcquireCommands(node.ID);
            }

            //// 下发部署的应用服务
            //rs.Services = GetServices(node.ID);
        }

        return rs;
    }

    private static Int32 _totalCommands;
    private static IList<NodeCommand> _commands;
    private static DateTime _nextTime;

    private static CommandModel[] AcquireCommands(Int32 nodeId)
    {
        // 缓存最近1000个未执行命令，用于快速过滤，避免大量节点在线时频繁查询命令表
        if (_nextTime < DateTime.Now || _totalCommands != NodeCommand.Meta.Count)
        {
            _totalCommands = NodeCommand.Meta.Count;
            _commands = NodeCommand.AcquireCommands(-1, 1000);
            _nextTime = DateTime.Now.AddMinutes(1);
        }

        // 是否有本节点
        if (!_commands.Any(e => e.NodeID == nodeId)) return null;

        var cmds = NodeCommand.AcquireCommands(nodeId, 100);
        if (cmds.Count == 0) return null;

        var rs = new List<CommandModel>();
        foreach (var item in cmds)
        {
            if (item.Times > 10 || item.Expire.Year > 2000 && item.Expire < DateTime.Now)
                item.Status = CommandStatus.取消;
            else
            {
                if (item.Status == CommandStatus.处理中 && item.UpdateTime.AddMinutes(10) < DateTime.Now) continue;

                item.Times++;
                item.Status = CommandStatus.处理中;
                rs.Add(item.ToModel());
            }
            item.UpdateTime = DateTime.Now;
        }
        cmds.Update(false);

        return rs.ToArray();
    }

    //private ServiceInfo[] GetServices(Int32 nodeId)
    //{
    //    var list = AppDeployNode.FindAllByNodeId(nodeId);
    //    list = list.Where(e => e.Enable).ToList();
    //    if (list.Count == 0) return null;

    //    var svcs = new List<ServiceInfo>();
    //    foreach (var item in list)
    //    {
    //        var deploy = item.App;
    //        if (deploy == null || !deploy.Enable) continue;

    //        svcs.Add(item.ToService());
    //    }

    //    return svcs.ToArray();
    //}

    public PingResponse Ping()
    {
        return new PingResponse
        {
            Time = 0,
            ServerTime = DateTime.Now,
        };
    }

    public NodeOnline GetOrAddOnline(Node node, String token, String ip)
    {
        var localIp = node?.IP;
        if (localIp.IsNullOrEmpty()) localIp = ip;

        return GetOnline(node, localIp) ?? CreateOnline(node, token, ip);
    }

    /// <summary></summary>
    /// <param name="node"></param>
    /// <returns></returns>
    public NodeOnline GetOnline(Node node, String ip)
    {
        //var sid = $"{node.ID}@{ip}";
        var sid = node.Code;
        var olt = _cache.Get<NodeOnline>($"NodeOnline:{sid}");
        if (olt != null)
        {
            _cache.SetExpire($"NodeOnline:{sid}", TimeSpan.FromSeconds(120));
            return olt;
        }

        return NodeOnline.FindBySessionID(sid);
    }

    /// <summary>检查在线</summary>
    /// <param name="node"></param>
    /// <returns></returns>
    public NodeOnline CreateOnline(Node node, String token, String ip)
    {
        //var sid = $"{node.ID}@{ip}";
        var sid = node.Code;
        var olt = NodeOnline.GetOrAdd(sid);
        olt.NodeID = node.ID;
        olt.Name = node.Name;
        olt.IP = node.IP;
        olt.Category = node.Category;
        olt.ProvinceID = node.ProvinceID;
        olt.CityID = node.CityID;
        olt.OSKind = node.OSKind;
        olt.Version = node.Version;
        olt.CompileTime = node.CompileTime;
        olt.Memory = node.Memory;
        olt.MACs = node.MACs;
        //olt.COMs = node.COMs;
        olt.Token = token;
        olt.CreateIP = ip;

        olt.Creator = Environment.MachineName;

        _cache.Set($"NodeOnline:{sid}", olt, 120);

        return olt;
    }
    #endregion

    #region 历史
    /// <summary>设备端响应服务调用</summary>
    /// <param name="model">服务</param>
    /// <returns></returns>
    public Int32 CommandReply(Node node, CommandReplyModel model, String token)
    {
        var cmd = NodeCommand.FindById(model.Id);
        if (cmd == null) return 0;

        cmd.Status = model.Status;
        cmd.Result = model.Data;
        cmd.Update();

        // 通知命令发布者，指令已完成
        var topic = $"nodereply:{cmd.Id}";
        var q = _queue.GetQueue<CommandReplyModel>(topic);
        q.Add(model);

        _queue.SetExpire(topic, TimeSpan.FromSeconds(60));

        return 1;
    }
    #endregion

    #region 升级
    /// <summary>升级检查</summary>
    /// <param name="channel">更新通道</param>
    /// <returns></returns>
    public NodeVersion Upgrade(Node node, String channel, String ip)
    {
        // 默认Release通道
        if (!Enum.TryParse<NodeChannels>(channel, true, out var ch)) ch = NodeChannels.Release;
        if (ch < NodeChannels.Release) ch = NodeChannels.Release;

        // 找到所有产品版本
        var list = NodeVersion.GetValids(ch);

        // 应用过滤规则，使用最新的一个版本
        var pv = list.Where(e => e.Match(node)).OrderByDescending(e => e.ID).FirstOrDefault();
        if (pv == null) return null;
        //if (pv == null) throw new ApiException(509, "没有升级规则");

        // 检查是否已经升级过这个版本
        if (node.LastVersion == pv.Version) return null;

        node.LastVersion = pv.Version;
        node.Update();

        node.WriteHistory("自动更新", true, $"channel={ch} => [{pv.ID}] {pv.Version} {pv.Executor}", ip);

        return pv;
    }
    #endregion

    #region 下行指令
    /// <summary>向节点发送命令。通知节点更新、安装和启停应用等</summary>
    /// <param name="model"></param>
    /// <param name="token">应用令牌</param>
    /// <returns></returns>
    public async Task<Int32> SendCommand(CommandInModel model, String token, StarServerSetting setting)
    {
        if (model.Code.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Code), "必须指定节点");
        if (model.Command.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Command));

        var node = Node.FindByCode(model.Code);
        if (node == null) throw new ArgumentOutOfRangeException(nameof(model.Code), "无效节点");

        var (_, app) = _tokenService.DecodeToken(token, setting.TokenSecret);
        if (app == null || app.AllowControlNodes.IsNullOrEmpty()) throw new ApiException(401, "无权操作！");

        if (app.AllowControlNodes != "*" && !node.Code.EqualIgnoreCase(app.AllowControlNodes.Split(",")))
            throw new ApiException(403, $"[{app}]无权操作节点[{node}]！");

        var cmd = new NodeCommand
        {
            NodeID = node.ID,
            Command = model.Command,
            Argument = model.Argument,
            Times = 0,
            Status = CommandStatus.就绪,

            TraceId = DefaultSpan.Current?.TraceId,
            CreateUser = app.Name,
        };
        if (model.Expire > 0) cmd.Expire = DateTime.Now.AddSeconds(model.Expire);
        cmd.Insert();

        var commandModel = cmd.ToModel();
        commandModel.TraceId = DefaultSpan.Current + "";

        var queue = _queue.GetQueue<String>($"nodecmd:{node.Code}");
        queue.Add(commandModel.ToJson());

        // 挂起等待。借助redis队列，等待响应
        if (model.Expire > 0)
        {
            var q = _queue.GetQueue<CommandReplyModel>($"nodereply:{cmd.Id}");
            var reply = await q.TakeOneAsync(model.Expire);
            if (reply != null)
            {
                // 埋点
                using var span = _tracer?.NewSpan($"redismq:ServiceLog", reply);

                if (reply.Status == CommandStatus.错误)
                    throw new Exception($"命令错误！{reply.Data}");
                else if (reply.Status == CommandStatus.取消)
                    throw new Exception($"命令已取消！{reply.Data}");
            }
        }

        return cmd.Id;
    }
    #endregion

    #region 辅助
    public TokenModel IssueToken(String name, StarServerSetting set)
    {
        // 颁发令牌
        var ss = set.TokenSecret.Split(':');
        var jwt = new JwtBuilder
        {
            Issuer = Assembly.GetEntryAssembly().GetName().Name,
            Subject = name,
            Id = Rand.NextString(8),
            Expire = DateTime.Now.AddSeconds(set.TokenExpire),

            Algorithm = ss[0],
            Secret = ss[1],
        };

        return new TokenModel
        {
            AccessToken = jwt.Encode(null),
            TokenType = jwt.Type ?? "JWT",
            ExpireIn = set.TokenExpire,
            RefreshToken = jwt.Encode(null),
        };
    }

    public (Node, Exception) DecodeToken(String token, String tokenSecret)
    {
        if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token));
        //if (token.IsNullOrEmpty()) throw new ApiException(401, $"节点未登录[ip={UserHost}]");

        // 解码令牌
        var ss = tokenSecret.Split(':');
        var jwt = new JwtBuilder
        {
            Algorithm = ss[0],
            Secret = ss[1],
        };

        var rs = jwt.TryDecode(token, out var message);
        var node = Node.FindByCode(jwt.Subject);

        Exception ex = null;
        if (!rs || node == null)
        {
            if (node != null)
                ex = new ApiException(403, $"[{node.Name}/{node.Code}]非法访问 {message}");
            else
                ex = new ApiException(403, $"[{jwt.Subject}]非法访问 {message}");
        }

        return (node, ex);
    }

    public TokenModel ValidAndIssueToken(String deviceCode, String token, StarServerSetting set)
    {
        if (token.IsNullOrEmpty()) return null;
        //var set = Setting.Current;

        // 令牌有效期检查，10分钟内过期者，重新颁发令牌
        var ss = set.TokenSecret.Split(':');
        var jwt = new JwtBuilder
        {
            Algorithm = ss[0],
            Secret = ss[1],
        };
        var rs = jwt.TryDecode(token, out var message);
        return !rs || jwt == null ? null : DateTime.Now.AddMinutes(10) > jwt.Expire ? IssueToken(deviceCode, set) : null;
    }

    //private void WriteHistory(Node node, String action, Boolean success, String remark, String ip)
    //{
    //    var hi = NodeHistory.Create(node, action, success, remark, Environment.MachineName, ip);
    //    hi.SaveAsync();
    //}
    #endregion
}