package com.usky.eg.service.impl;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.usky.common.core.exception.BusinessException;
import com.usky.eg.adb.EgDeviceAdbProperties;
import com.usky.eg.service.EgDeviceAdbService;
import com.usky.eg.service.vo.EgDeviceAdbResultVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 通过 {@link ProcessBuilder} 调用本机 PATH 中的官方 adb。
 */
@Slf4j
@Service
public class EgDeviceAdbServiceImpl implements EgDeviceAdbService {

    private static final int BUFFER = 16384;

    /**
     * Windows 下 adb 常输出系统本地编码（如 GBK），勿固定按 UTF-8 解码以免日志/接口里出现乱码。
     */
    private static final Charset ADB_CONSOLE_CHARSET = Charset.defaultCharset();

    private static final String NETWORK_TIMEOUT_HINT =
            " 【排查】运行本服务的机器须与设备网络互通并能访问目标 TCP 端口（常见 5555）；"
                    + "检查防火墙/安全组/跨网段路由；确认手机「无线调试」已开启且端口与请求一致；"
                    + "Android 11+ 有时需先用系统界面配对，端口可能不是 5555；"
                    + "错误 10060 表示连接超时，多为网络不可达或端口未监听。";

    @Autowired
    private EgDeviceAdbProperties properties;

    @Override
    public EgDeviceAdbResultVO connect(String deviceIp, Integer devicePort) {
        validateEndpoint(deviceIp, devicePort);
        String endpoint = toEndpoint(deviceIp, devicePort);
        try {
            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
                    Arrays.asList(properties.getExecutable(), "connect", endpoint));
            return EgDeviceAdbResultVO.ok(out);
        } catch (Exception e) {
            log.warn("adb connect failed: {}", endpoint, e);
            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
        }
    }

    @Override
    public EgDeviceAdbResultVO disconnect(String deviceIp, Integer devicePort) {
        validateEndpoint(deviceIp, devicePort);
        String endpoint = toEndpoint(deviceIp, devicePort);
        try {
            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
                    Arrays.asList(properties.getExecutable(), "disconnect", endpoint));
            return EgDeviceAdbResultVO.ok(out);
        } catch (Exception e) {
            log.warn("adb disconnect failed: {}", endpoint, e);
            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
        }
    }

    @Override
    public EgDeviceAdbResultVO tcpip(String deviceIp, Integer devicePort, Integer listenPort) {
        validateEndpoint(deviceIp, devicePort);
        if (listenPort == null || listenPort < 1 || listenPort > 65535) {
            throw new BusinessException("port 无效（tcpip 监听端口）");
        }
        String serial = toSerial(deviceIp, devicePort);
        EgDeviceAdbResultVO first = runTcpipOnce(serial, listenPort);
        if (EgDeviceAdbResultVO.STATUS_SUCCESS.equals(first.getStatus())) {
            return first;
        }
        if (!properties.isTcpipRetryAfterConnect()) {
            return first;
        }
        log.info("adb tcpip 首次失败，尝试 connect 后重试: {}", serial);
        EgDeviceAdbResultVO c = connect(deviceIp, devicePort);
        if (!EgDeviceAdbResultVO.STATUS_SUCCESS.equals(c.getStatus())) {
            return EgDeviceAdbResultVO.fail("tcpip 失败且自动 connect 未成功: " + first.getDetail() + " | connect: " + c.getDetail());
        }
        return runTcpipOnce(serial, listenPort);
    }

    private EgDeviceAdbResultVO runTcpipOnce(String serial, int listenPort) {
        try {
            String out = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
                    Arrays.asList(properties.getExecutable(), "-s", serial, "tcpip", String.valueOf(listenPort)));
            return EgDeviceAdbResultVO.ok(out);
        } catch (Exception e) {
            log.warn("adb tcpip failed: {} {}", serial, listenPort, e);
            return EgDeviceAdbResultVO.fail(e.getMessage() != null ? e.getMessage() : e.toString());
        }
    }

    @Override
    public byte[] screenshot(String deviceIp, Integer devicePort) {
        validateEndpoint(deviceIp, devicePort);
        String serial = toSerial(deviceIp, devicePort);
        if (properties.isScreenshotConnectFirst()) {
            try {
                ensureAdbDeviceOnline(deviceIp, devicePort);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new BusinessException("设备未就绪，无法截屏: " + e.getMessage());
            } catch (IOException e) {
                throw new BusinessException("设备未就绪，无法截屏: " + e.getMessage());
            }
        }
        List<String> cmd = Arrays.asList(
                properties.getExecutable(),
                "-s", serial,
                "exec-out", "screencap", "-p");
        try {
            byte[] png = runAdbBinaryStdout(cmd, properties.getScreenshotTimeoutSeconds());
            checkPng(png);
            return png;
        } catch (Exception e) {
            log.warn("adb screencap failed: {}", serial, e);
            throw new BusinessException("截屏失败: " + e.getMessage()
                    + "（请确认设备已开无线调试、端口正确，或先调用 /egDevice/adb/connect）");
        }
    }

    /**
     * 让本机 adb 真正识别该 serial：执行 connect、校验 {@code adb devices} 为 {@code device} 状态。
     * 若仅“假装 connect”或忽略失败，会继续出现 {@code device 'ip:port' not found}。
     */
    private void ensureAdbDeviceOnline(String deviceIp, int devicePort) throws IOException, InterruptedException {
        String serial = toSerial(deviceIp, devicePort);
        String endpoint = toEndpoint(deviceIp, devicePort);

        String connectOut = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
                Arrays.asList(properties.getExecutable(), "connect", endpoint));
        if (connectOutputIndicatesFailure(connectOut)) {
            throw new IOException(withTimeoutHint("adb connect 输出表明失败: " + connectOut));
        }

        if (waitUntilDeviceOnline(serial, 3, 400)) {
            Thread.sleep(200);
            return;
        }

        log.warn("首次 connect 后 devices 仍未就绪，重试 connect: {}", endpoint);
        connectOut = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
                Arrays.asList(properties.getExecutable(), "connect", endpoint));
        if (connectOutputIndicatesFailure(connectOut)) {
            throw new IOException(withTimeoutHint("adb connect（重试）失败: " + connectOut));
        }
        if (!waitUntilDeviceOnline(serial, 5, 500)) {
            String devicesList = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
                    Arrays.asList(properties.getExecutable(), "devices", "-l"));
            throw new IOException(withTimeoutHint("adb devices 中无可用设备 " + serial
                    + "。connect 输出: " + connectOut + "；当前列表:\n" + devicesList));
        }
        Thread.sleep(200);
    }

    private boolean waitUntilDeviceOnline(String serial, int attempts, long gapMs) throws IOException, InterruptedException {
        for (int i = 0; i < attempts; i++) {
            if (i > 0) {
                Thread.sleep(gapMs);
            }
            String list = runAdbMergedOutput(properties.getCommandTimeoutSeconds(),
                    Arrays.asList(properties.getExecutable(), "devices"));
            String state = findSerialState(list, serial);
            if ("device".equals(state)) {
                return true;
            }
            if ("unauthorized".equals(state)) {
                throw new IOException("设备已发现但未授权调试，请在设备上点「允许 USB 调试」: " + serial);
            }
            if ("offline".equals(state)) {
                log.warn("设备当前为 offline: {}", serial);
            }
        }
        return false;
    }

    /**
     * 解析 {@code adb devices}，返回 serial 对应第二列状态；未找到返回 null
     */
    private static String findSerialState(String devicesOutput, String serial) {
        if (devicesOutput == null) {
            return null;
        }
        for (String raw : devicesOutput.split("\\r?\\n")) {
            String line = raw.trim();
            if (line.isEmpty() || line.startsWith("List of devices")) {
                continue;
            }
            String[] parts = line.split("\\s+");
            if (parts.length >= 2 && serial.equals(parts[0])) {
                return parts[1];
            }
        }
        return null;
    }

    private static boolean connectOutputIndicatesFailure(String connectOut) {
        if (connectOut == null || connectOut.isEmpty()) {
            return false;
        }
        String s = connectOut.toLowerCase();
        return s.contains("cannot connect")
                || s.contains("failed to connect")
                || s.contains("unable to connect")
                || s.contains("connection refused")
                || s.contains("10061")
                || s.contains("10060")
                || s.contains("timed out")
                || connectOut.contains("连接尝试失败")
                || connectOut.contains("没有正确答复");
    }

    private static String withTimeoutHint(String message) {
        if (message == null) {
            return NETWORK_TIMEOUT_HINT.trim();
        }
        String m = message;
        String lower = m.toLowerCase();
        if (lower.contains("10060") || lower.contains("10061") || lower.contains("cannot connect")
                || lower.contains("timed out") || m.contains("连接尝试失败") || m.contains("没有正确答复")) {
            return m + NETWORK_TIMEOUT_HINT;
        }
        return m;
    }

    /**
     * adb 文本类输出（stdout/stderr）按 JVM 默认字符集解码
     */
    private static String decodeAdbConsole(byte[] raw) {
        if (raw == null || raw.length == 0) {
            return "";
        }
        return new String(raw, ADB_CONSOLE_CHARSET).trim();
    }

    private static void validateEndpoint(String deviceIp, Integer devicePort) {
        if (StringUtils.isBlank(deviceIp)) {
            throw new BusinessException("deviceIp 不能为空");
        }
        if (devicePort == null || devicePort < 1 || devicePort > 65535) {
            throw new BusinessException("devicePort 无效");
        }
    }

    private static String toEndpoint(String deviceIp, int devicePort) {
        return deviceIp.trim() + ":" + devicePort;
    }

    private static String toSerial(String deviceIp, int devicePort) {
        return toEndpoint(deviceIp, devicePort);
    }

    /**
     * 合并 stdout/stderr，非 0 退出码抛出异常（文本类命令）
     */
    private String runAdbMergedOutput(long timeoutSeconds, List<String> command) throws IOException, InterruptedException {
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.redirectErrorStream(true);
        Process p = pb.start();
        byte[] raw = readAll(p.getInputStream());
        String text = decodeAdbConsole(raw);
        boolean finished = p.waitFor(timeoutSeconds, TimeUnit.SECONDS);
        if (!finished) {
            p.destroyForcibly();
            throw new IOException("adb 执行超时（" + timeoutSeconds + "s）");
        }
        int code = p.exitValue();
        if (code != 0) {
            throw new IOException(withTimeoutHint("adb 退出码 " + code + (text.isEmpty() ? "" : (": " + text))));
        }
        return text.isEmpty() ? "OK" : text;
    }

    private byte[] runAdbBinaryStdout(List<String> command, long timeoutSeconds) throws IOException, InterruptedException {
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.redirectErrorStream(false);
        Process p = pb.start();
        byte[] out = readAll(p.getInputStream());
        byte[] err = readAll(p.getErrorStream());
        boolean finished = p.waitFor(timeoutSeconds, TimeUnit.SECONDS);
        if (!finished) {
            p.destroyForcibly();
            throw new IOException("adb 执行超时（" + timeoutSeconds + "s）");
        }
        int code = p.exitValue();
        String errStr = err.length == 0 ? "" : decodeAdbConsole(err);
        if (code != 0) {
            throw new IOException(withTimeoutHint("adb 退出码 " + code + (errStr.isEmpty() ? "" : (": " + errStr))));
        }
        if (out.length == 0 && !errStr.isEmpty()) {
            throw new IOException(withTimeoutHint(errStr));
        }
        return out;
    }

    private static byte[] readAll(InputStream in) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buf = new byte[BUFFER];
        int n;
        while ((n = in.read(buf)) != -1) {
            bos.write(buf, 0, n);
        }
        return bos.toByteArray();
    }

    private static void checkPng(byte[] data) {
        if (data == null || data.length < 24) {
            throw new BusinessException("截屏数据无效（过小）");
        }
        if (data[0] != (byte) 0x89 || data[1] != 0x50 || data[2] != 0x4E || data[3] != 0x47) {
            throw new BusinessException("截屏数据不是有效的 PNG");
        }
    }
}
