package com.seer.teach.netty.im.utils;

import com.alibaba.fastjson2.JSON;
import com.seer.teach.common.enums.ResultCodeEnum;
import com.seer.teach.common.exception.BizException;
import com.seer.teach.common.exception.TvNotOnlineException;
import com.seer.teach.netty.common.proto.MessageProtobuf;
import com.seer.teach.netty.common.proto.MessageType;
import com.seer.teach.netty.im.manager.AckSender;
import com.seer.teach.netty.im.manager.ImChannelManager;
import io.netty.channel.Channel;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Promise;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * NettyUtils
 * <p>Netty 工具类：负责服务端向设备推送消息、回执等的统一封装。</p>
 *
 * @author Captain
 * @version 1.2
 * @since 2025-07-14 20:51
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ImUtils {

    /**
     * 发送确认接收消息到指定设备。
     * <p>该方法用于构建并发送一个状态报告类型的消息，表示服务端已成功接收到客户端发送的消息。</p>
     *
     * @param device 设备标识符，用于定位目标设备通道
     * @param msg    原始消息对象，用于提取消息ID和内容类型等信息
     */
    public static void sendConfirmationOfAcceptanceMessageToDevice(String device, MessageProtobuf.Msg msg) {
        MessageProtobuf.Msg.Builder pushMessageBuilder = MessageProtobuf.Msg.newBuilder();
        MessageProtobuf.Head.Builder pushMessageHeadBuilder = MessageProtobuf.Head.newBuilder();
        pushMessageHeadBuilder.setMsgId(msg.getHead().getMsgId());
        pushMessageHeadBuilder.setMsgType(MessageType.SERVER_MSG_SENT_STATUS_REPORT.getMsgType());
        pushMessageHeadBuilder.setMsgContentType(msg.getHead().getMsgContentType());
        pushMessageHeadBuilder.setTimestamp(System.currentTimeMillis());
        pushMessageHeadBuilder.setStatusReport(1);
        pushMessageHeadBuilder.setDevice(device);
        pushMessageBuilder.setHead(pushMessageHeadBuilder);
        sendToTvByDevice(pushMessageBuilder);
    }

    /**
     * 向指定设备推送单聊消息（使用 Object 类型结果）。
     * <p>该方法将传入的结果对象序列化为 JSON 字符串后发送给设备。</p>
     *
     * @param device             设备标识符，用于定位目标设备通道
     * @param statusReport       状态报告标志位，0 表示不需要回执，1 表示需要回执
     * @param messageContentType 消息内容类型，用于标识消息体的数据格式或业务含义
     * @param results            消息体内容对象，将被序列化成 JSON 字符串发送
     * @throws BizException 当设备不在线或通道无效时抛出异常
     */
    public static void pushSingleChatToDevice(String device,
                                              Integer statusReport,
                                              Integer messageContentType,
                                              Object results) throws BizException {
        MessageProtobuf.Msg.Builder builder = commonSingleChatHeader(device, statusReport, messageContentType);
        builder.setBody(JSON.toJSONString(results));
        sendToTvByDevice(builder);
    }


    /**
     * 向指定设备推送单聊消息（使用 Object 类型结果）携带ACK确认机制。
     * <p>该方法将传入的结果对象序列化为 JSON 字符串后发送给设备。</p>
     *
     * @param device             设备标识符，用于定位目标设备通道
     * @param statusReport       状态报告标志位，0 表示不需要回执，1 表示需要回执
     * @param messageContentType 消息内容类型，用于标识消息体的数据格式或业务含义
     * @param results            消息体内容对象，将被序列化成 JSON 字符串发送
     * @throws BizException 当设备不在线或通道无效时抛出异常
     */
    public static void pushSingleChatToDeviceWithAck(String device,
                                                     Integer statusReport,
                                                     Integer messageContentType,
                                                     Object results) throws BizException {
        MessageProtobuf.Msg.Builder builder = commonSingleChatHeader(device, statusReport, messageContentType);
        builder.setBody(JSON.toJSONString(results));
        sendToTvByDeviceWithAck(builder);
    }

    /**
     * 向指定设备推送单聊消息（使用 Object 类型结果）携带ACK确认机制。
     * <p>该方法将传入的结果对象序列化为 JSON 字符串后发送给设备。</p>
     *
     * @param device             设备标识符，用于定位目标设备通道
     * @param statusReport       状态报告标志位，0 表示不需要回执，1 表示需要回执
     * @param messageContentType 消息内容类型，用于标识消息体的数据格式或业务含义
     * @param results            消息体内容对象，将被序列化成 JSON 字符串发送
     * @throws BizException 当设备不在线或通道无效时抛出异常
     */
    public static long pushSingleChatToDeviceWithAckTime(String device,
                                                         Integer statusReport,
                                                         Integer messageContentType,
                                                         Object results) throws BizException {
        MessageProtobuf.Msg.Builder builder = commonSingleChatHeader(device, statusReport, messageContentType);
        builder.setBody(JSON.toJSONString(results));
        return sendToTvByDeviceWithAckBlocking(builder);
    }

    public static void pushSingleChatToDeviceWithAck(String device,
                                                     Integer statusReport,
                                                     Integer messageContentType,
                                                     String results) throws BizException {
        MessageProtobuf.Msg.Builder builder = commonSingleChatHeader(device, statusReport, messageContentType);
        builder.setBody(results);
        sendToTvByDeviceWithAck(builder);
    }


    /**
     * 向指定设备推送单聊消息（使用 String 类型结果）。
     * <p>该方法直接使用传入的字符串作为消息体发送给设备。</p>
     *
     * @param device             设备标识符，用于定位目标设备通道
     * @param statusReport       状态报告标志位，0 表示不需要回执，1 表示需要回执
     * @param messageContentType 消息内容类型，用于标识消息体的数据格式或业务含义
     * @param results            消息体内容字符串，将直接作为消息体发送
     * @throws BizException 当设备不在线或通道无效时抛出异常
     */
    public static void pushSingleChatToDevice(String device,
                                              Integer statusReport,
                                              Integer messageContentType,
                                              String results) throws BizException {
        MessageProtobuf.Msg.Builder builder = commonSingleChatHeader(device, statusReport, messageContentType);
        builder.setBody(results);
        sendToTvByDevice(builder);
    }

    /**
     * 将构建好的消息通过 Netty 通道发送到指定设备。
     * <p>该方法会阻塞等待 ACK 确认结果，并返回从发送到收到确认的耗时（毫秒）。</p>
     *
     * @param sentReportMsgBuilder 待发送的消息构建器
     * @return long 延时（ms），表示从发送到收到 ACK 的耗时
     * @throws TvNotOnlineException 当设备不在线时抛出异常
     * @throws BizException         当发送失败或超时时抛出自定义业务异常
     */
    public static long sendToTvByDeviceWithAckBlocking(MessageProtobuf.Msg.Builder sentReportMsgBuilder) {
        final long begin = System.currentTimeMillis();
        MessageProtobuf.Head head = sentReportMsgBuilder.getHead();
        String device = head.getDevice();
        String msgId = head.getMsgId();
        Integer msgContentType = head.getMsgContentType();
        try {
            // 检查通道是否可用
            Channel channel = ImChannelManager.get(device);
            if (channel == null || !channel.isActive()) {
                log.warn("[IM-SEND] 通道不存在或不可用，device={}", device);
                throw new TvNotOnlineException(ResultCodeEnum.TV_NOT_ONLINE.getCode(), "设备不在线或通道已失效");
            }

            byte[] bodyBytes = safeBodyBytes(sentReportMsgBuilder);
            final EventExecutor executor = channel.eventLoop();

            // 初始化 ACK 发送器：3次重试，每次间隔3秒，单次ACK超时500ms
            final AckSender ackSender = new AckSender(executor, 3, 3000, 3000);
            Promise<Boolean> ackPromise = ackSender.sendWithAck(device, sentReportMsgBuilder.build());

            // 阻塞等待 ACK 结果，最长等待10秒
            boolean ackSuccess = ackPromise.await(10, TimeUnit.SECONDS) && Boolean.TRUE.equals(ackPromise.getNow());
            long cost = System.currentTimeMillis() - begin;

            // 打印发送日志
            log.info("\n====== IM-发送 成功 ======\n" +
                            "设备           : {}\n" +
                            "消息ID         : {}\n" +
                            "MsgContentType : {}\n" +
                            "耗时 (ms)      : {}\n" +
                            "消息字节数     : {}\n" +
                            "通道ID        : {}\n" +
                            "======================",
                    device,
                    msgId,
                    msgContentType,
                    cost,
                    (bodyBytes == null ? 0 : bodyBytes.length),
                    channel.id().asShortText());

            if (ackSuccess) {
                return cost;
            } else {
                throw new BizException(ResultCodeEnum.IM_PUSH_FAIL.getCode(),
                        "IM 推送超时或未确认成功 (msgId=" + msgId + ")");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 将构建好的消息通过 Netty 通道发送到指定设备。
     * <p>该方法会检查设备对应的通道是否可用，并记录发送过程中的日志及性能指标。携带ACK确认机制</p>
     *
     * @param sentReportMsgBuilder 待发送的消息构建器
     * @throws TvNotOnlineException 当设备不在线时抛出异常
     * @throws BizException         当发送失败时抛出自定义业务异常
     */
    public static void sendToTvByDeviceWithAck(MessageProtobuf.Msg.Builder sentReportMsgBuilder) {
        final long begin = System.currentTimeMillis();
        MessageProtobuf.Head head = sentReportMsgBuilder.getHead();
        String device = head.getDevice();
        String msgId = head.getMsgId();
        Integer msgContentType = head.getMsgContentType();
        Channel channel = ImChannelManager.get(device);

        if (channel == null || !channel.isActive()) {
            log.warn("\n====== IM-发送 警告 ======\n" +
                            "设备       : {}\n" +
                            "原因       : 通道不存在或不可用\n" +
                            "======================",
                    device);
            throw new TvNotOnlineException(ResultCodeEnum.TV_NOT_ONLINE.getCode(),
                    "设备不在线或通道已失效");
        }

        byte[] bodyBytes = safeBodyBytes(sentReportMsgBuilder);
        final EventExecutor executor = channel.eventLoop();
        final AckSender ackSender = new AckSender(executor, 3, 3000, 500);
        Promise<Boolean> ackPromise = ackSender.sendWithAck(device, sentReportMsgBuilder.build());

        ackPromise.addListener(f -> {
            long cost = System.currentTimeMillis() - begin;

            if (f.isSuccess() && Boolean.TRUE.equals(ackPromise.getNow())) {
                log.info("\n====== IM-发送 成功 ======\n" +
                                "设备           : {}\n" +
                                "消息ID         : {}\n" +
                                "MsgContentType : {}\n" +
                                "耗时 (ms)      : {}\n" +
                                "消息字节数     : {}\n" +
                                "通道ID        : {}\n" +
                                "======================",
                        device,
                        msgId,
                        msgContentType,
                        cost,
                        (bodyBytes == null ? 0 : bodyBytes.length),
                        channel.id().asShortText());
            } else {
                Throwable cause = f.cause();
                log.error("\n====== IM-发送 失败 ======\n" +
                                "设备       : {}\n" +
                                "消息ID     : {}\n" +
                                "耗时 (ms)  : {}\n" +
                                "错误原因   : {}\n" +
                                "======================",
                        device, msgId, cost,
                        (cause == null ? "unknown" : cause.toString()), cause);

                // 保持原行为：回调中抛业务异常（注意：抛出发生在 Netty 线程）
                throw new BizException(ResultCodeEnum.IM_PUSH_FAIL.getCode(),
                        "IM 推送未确认或失败：" + (cause == null ? "unknown" : cause.getMessage()));
            }
        });
    }

    /**
     * 将构建好的消息通过 Netty 通道发送到指定设备。
     * <p>该方法会检查设备对应的通道是否可用，并记录发送过程中的日志及性能指标。</p>
     *
     * @param sentReportMsgBuilder 待发送的消息构建器
     * @throws TvNotOnlineException 当设备不在线时抛出异常
     * @throws BizException         当发送失败时抛出自定义业务异常
     */
    public static void sendToTvByDevice(MessageProtobuf.Msg.Builder sentReportMsgBuilder) {
        MessageProtobuf.Head head = sentReportMsgBuilder.getHead();
        String device = head.getDevice();
        Channel channel = ImChannelManager.get(device);
        if (channel == null || !channel.isActive()) {
            log.warn("[IM-SEND] 通道不存在或不可用，device={}", device);
            throw new TvNotOnlineException(ResultCodeEnum.TV_NOT_ONLINE.getCode(), "设备不在线或通道已失效");
        }
        channel.writeAndFlush(sentReportMsgBuilder);
    }


    /**
     * 构建通用的单聊消息头部。
     * <p>该方法用于初始化一个单聊消息的基本头部结构，包括消息ID、类型、时间戳、状态报告标志、设备ID和内容类型。</p>
     *
     * @param device             目标设备标识符
     * @param statusReport       状态报告标志位，0 表示不需要回执，1 表示需要回执
     * @param messageContentType 消息内容类型
     * @return 返回初始化完成的消息构建器
     */
    public static MessageProtobuf.Msg.Builder commonSingleChatHeader(String device,
                                                                      Integer statusReport,
                                                                      Integer messageContentType) {
        MessageProtobuf.Msg.Builder msgBuilder = MessageProtobuf.Msg.newBuilder();
        MessageProtobuf.Head.Builder headBuilder = MessageProtobuf.Head.newBuilder();
        headBuilder.setMsgId(UUID.randomUUID().toString());
        headBuilder.setMsgType(MessageType.SINGLE_CHAT.getMsgType());
        headBuilder.setTimestamp(System.currentTimeMillis());
        headBuilder.setStatusReport(statusReport == null ? 0 : statusReport);
        headBuilder.setDevice(device);
        headBuilder.setMsgContentType(messageContentType == null ? 0 : messageContentType);
        msgBuilder.setHead(headBuilder.build());
        return msgBuilder;
    }

    /**
     * 安全地获取消息体字节数组。
     * <p>该方法尝试将消息体转换为字节数组，若发生异常则返回 null。</p>
     *
     * @param builder 消息构建器
     * @return 消息体的字节数组，若无法转换则返回 null
     */
    private static byte[] safeBodyBytes(MessageProtobuf.Msg.Builder builder) {
        try {
            String body = builder.getBody();
            return body == null ? null : body.getBytes();
        } catch (Throwable ignore) {
            return null;
        }
    }

    /**
     * 将 CPU 使用率字符串转换为可比较的浮点数
     * 示例输入: "-1.0"
     */
    public static double parseCpuUsage(String cpuUsage) {
        try {
            return Double.parseDouble(cpuUsage);
        } catch (NumberFormatException e) {
            // 出错返回 -1 表示不可用
            return -1;
        }
    }

    /**
     * 将内存使用信息字符串解析为使用率百分比
     * 示例输入: "总内存:6.22 GB, 可用内存:3.92 GB, 使用率:37.0%"
     */
    public static double parseMemoryUsage(String memoryUsage) {
        try {
            int index = memoryUsage.indexOf("使用率:");
            if (index == -1) return -1;

            String usageStr = memoryUsage.substring(index + "使用率:".length())
                    .replace("%", "")
                    .trim();
            return Double.parseDouble(usageStr);
        } catch (Exception e) {
            return -1;
        }
    }
}
