package io.wdd.func.xray.service;


import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import io.wdd.common.handler.MyRuntimeException;
import io.wdd.func.xray.beans.node.ProxyNode;
import io.wdd.func.xray.beans.node.XrayConfigInfo;
import io.wdd.func.xray.beans.xray.XrayConfig;
import io.wdd.func.xray.beans.xray.share.VmessShare;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static io.wdd.common.utils.TimeUtils.UniformFormatTimeString;
import static io.wdd.func.xray.persisit.cache.ProxyNodeCache.PROXY_NODE_MAP;
import static io.wdd.func.xray.persisit.cache.ProxyNodeCache.SUBSCRIBE_NODE_NAME;

/**
 * 获取rerouces目录 https://blog.csdn.net/pengpengpeng85/article/details/84785575
 * <p>
 * <p>
 * 写入文件的教程 https://cloud.tencent.com/developer/article/1895274
 */
@Slf4j
@Service
public class XrayConfigPersistor {

    /**
     * XRAY端口的起始位置
     */
    public static final int XRAY_PORT_MAX = 20000;
    // 参考 https://github.com/FasterXML/jackson-databind/issues/585
    private static final ObjectWriter objectWriter = new ObjectMapper()
            // 忽略掉 null的字段
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            // 写的文件必须是unix类型的分隔符号
            .writer(
                    new DefaultPrettyPrinter()
                            .withObjectIndenter(
                                    new DefaultIndenter()
                                            .withLinefeed("\n")
                            ));
    private static final String XrayResultPathPrefix = "xrayResult/";

    /**
     * 存储在对象存储中的订阅文件的名称
     */
    public static final String SUBSCRIBE_FILE_OSS_NAME = "99-subscribe-octopus-latest.txt";
    /**
     * 存储生成的Xray Config的临时文件目录
     */
    private static final File XrayResultPath;
    /**
     * 一个乐观锁，用于标定一次生成过程是否需要进行旧文件的清除
     */
    public static AtomicInteger cleanVersion = new AtomicInteger(0);
    /**
     * Xray的Port的池子， 一直递减
     * 当进行重置的时候，需要进行清空
     */
    public static AtomicInteger XRAY_PORT_POOL = new AtomicInteger(XRAY_PORT_MAX);

    static {
        ClassPathResource resource = new ClassPathResource(XrayResultPathPrefix);

        try {
            if (resource
                    .getURL()
                    .getPath()
                    .startsWith("file:/wdd/server")) {
                // SpringBoot 打了Jar包的形式
                File file = new File("/octopus-server/xray");
                if (!file.exists()) {
                    file.mkdirs();
                }

                XrayResultPath = file;
            } else {
                // 本地开发的模式
                try {
                    XrayResultPath = resource.getFile();
                } catch (IOException e) {
                    log.error("获取XrayConfig的临时文件目录失败！");
                    throw new MyRuntimeException(e);
                }
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 执行Xray生成配置文件的持久化工作,沈成伟临时文件保存至当前目录中
     *
     * @param xrayConfig
     * @param currentVersion
     * @param proxyNode
     * @return
     */
    public XrayConfigInfo persist(XrayConfig xrayConfig, int currentVersion, ProxyNode proxyNode) {

        // 从缓存层获取信息
        XrayConfigInfo xrayConfigInfo = proxyNode.getXrayConfigInfo();
        if (ObjectUtils.isEmpty(xrayConfigInfo)) {
            xrayConfigInfo = new XrayConfigInfo();
            proxyNode.setXrayConfigInfo(xrayConfigInfo);
        }

        try {
            // 将生成的xrayConfig直接写为字符串
            String resultContent = objectWriter
                    .writeValueAsString(xrayConfig);

            // 获得到文件名称
            String timeString = UniformFormatTimeString();
            String fileName = buildXrayConfigFileName(
                    proxyNode,
                    timeString
            );

            // 文件持久化!
            File xrayConfigFile = write(
                    fileName,
                    resultContent,
                    currentVersion
            );

            xrayConfigInfo.setXrayConfigFile(xrayConfigFile);
            xrayConfigInfo.setXrayConfigFileName(fileName);


            // 文件写入完成，保存文件信息
            xrayConfigInfo.setXrayConfig(xrayConfig);

            return xrayConfigInfo;

        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public void subPersist(List<VmessShare> vmessShareList) {

        Base64.Encoder encoder = Base64.getEncoder();

        List<String> subscribeStringList = vmessShareList
                .stream()
                .map(
                        vmessShare -> {

                            try {

                                byte[] vmessShareBytes = objectWriter.writeValueAsBytes(vmessShare);

                                return "vmess://" + new String(
                                        encoder.encode(vmessShareBytes),
                                        StandardCharsets.UTF_8
                                );

                            } catch (JsonProcessingException e) {
                                throw new RuntimeException(e);
                            }

                        }
                )
                .collect(Collectors.toList());

        // 实际进行文件的写入
        File subscribeFile = subWrite(
                SUBSCRIBE_FILE_OSS_NAME,
                subscribeStringList
        );

        // 存储相应的信息
        ProxyNode subscribeNode = PROXY_NODE_MAP.get(SUBSCRIBE_NODE_NAME);
        XrayConfigInfo xrayConfigInfo = subscribeNode.getXrayConfigInfo();

        xrayConfigInfo.setXrayConfigFile(subscribeFile);
        xrayConfigInfo.setXrayConfigFileName(SUBSCRIBE_FILE_OSS_NAME);

    }


    private String buildXrayConfigFileName(ProxyNode proxyNode, String timeString) {

        return proxyNode.getNum() + "-" + proxyNode.getAgentName() + "-" + timeString + ".json";

    }


    /**
     * 将订阅信息写入到文件中， 每一行写一个
     * 由于订阅信息的生成一定是晚于代理链的，所以不进行清理工作
     *
     * @param fileName
     * @return
     */
    private File subWrite(String fileName, List<String> subsribeList) {

        // 构造对象开始写入, 生成文件
        File resultFile = getResultFile(fileName);

        FileWriter fileWriter = null;
        BufferedWriter bufferedWriter = null;

        try {
            log.debug(
                    "开始写入 Subscribe 进入文件中，文件名为 => {}",
                    fileName
            );
            fileWriter = new FileWriter(
                    resultFile
            );
            bufferedWriter = new BufferedWriter(
                    fileWriter
            );


            for (String subsribeString : subsribeList) {

                // 每一行写一个
                bufferedWriter.write(subsribeString);
                bufferedWriter.newLine();

            }

            return resultFile;

        } catch (IOException e) {
            log.error(
                    "打开文件失败，写入tmp文件失败！ 文件为 => {}",
                    resultFile.getName()
            );
            throw new MyRuntimeException(e);
        } finally {

            try {
                // must close
                bufferedWriter.close();
                fileWriter.close();

            } catch (IOException e) {
                log.error(
                        "关闭文件写入流失败!, 请检查 文件为 => [ {} ], 内容为 => {}",
                        fileName,
                        subsribeList
                );
                throw new MyRuntimeException(e);
            }
        }

    }

    private File write(String fileName, String content, int currentVersion) {

        log.debug(
                "currentVersion = {}",
                currentVersion
        );

        if (cleanVersion.get() == currentVersion) {
            // 清除旧的内容
            clearOldRemainStaff();
            // 自增1，避免后续继续清除tmp目录
            cleanVersion.incrementAndGet();
        }

        // 构造对象开始写入, 生成文件
        File resultFile = getResultFile(fileName);

        FileWriter fileWriter = null;
        BufferedWriter bufferedWriter = null;

        try {
            log.debug(
                    "开始写入XrayConfig进入文件中，文件名为 => {}",
                    fileName
            );
            fileWriter = new FileWriter(
                    resultFile
            );
            bufferedWriter = new BufferedWriter(
                    fileWriter
            );

            log.debug(
                    "文件内容为 => {}",
                    content
            );
            bufferedWriter.write(content);

            return resultFile;

        } catch (IOException e) {
            log.error(
                    "打开文件失败，写入tmp文件失败！ 文件为 => {}",
                    resultFile.getName()
            );
            throw new MyRuntimeException(e);
        } finally {

            try {
                // must close
                bufferedWriter.close();
                fileWriter.close();

            } catch (IOException e) {
                log.error(
                        "关闭文件写入流失败!, 请检查 文件为 => [ {} ], 内容为 => {}",
                        fileName,
                        content
                );
                throw new MyRuntimeException(e);
            }
        }
    }

    private void clearOldRemainStaff() {

        try {
            FileUtils.cleanDirectory(
                    XrayResultPath
            );
        } catch (IOException e) {
            log.error("清楚旧的目录失败！ 请检查问题！");
            throw new MyRuntimeException(e);
        }


    }

    /**
     * 根据文件名，需要创建一个文件
     *
     * @param fileName 文件名，如 xxx.json
     * @return
     */
    private File getResultFile(String fileName) {

        // 打包之后 无法获取到包内的文件 https://blog.csdn.net/u013084266/article/details/112238267

        // 需要创建一个文件
        return new File(
                XrayResultPath,
                fileName
        );

    }


}
