package com.fdkankan.scene.download.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fdkankan.common.constant.ErrorCode;
import com.fdkankan.common.constant.ServerCode;
import com.fdkankan.common.constant.UploadFilePath;
import com.fdkankan.common.response.ResultData;
import com.fdkankan.common.util.FileUtils;
import com.fdkankan.fyun.constant.StorageType;
import com.fdkankan.fyun.oss.UploadToOssUtil;
import com.fdkankan.redis.constant.RedisKey;
import com.fdkankan.redis.util.RedisUtil;
import com.fdkankan.scene.download.bean.DownLoadProgressBean;
import com.fdkankan.scene.download.bean.DownLoadTaskBean;
import com.fdkankan.scene.download.bean.ImageType;
import com.fdkankan.scene.download.bean.ImageTypeDetail;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import lombok.var;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.tools.zip.ZipFile;
import org.apache.tools.zip.ZipOutputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * <p>
 * TODO
 * </p>
 *
 * @author dengsixing
 * @since 2022/2/22
 **/
@Slf4j
@Service
public class SceneDownloadHandlerServiceImpl {

    private static final String wwwroot = "wwwroot/";
    private static final String[] prefixArr = new String[]{
        "data/data%s/",
        "voice/voice%s/",
        "video/video%s/",
        "images/images%s/"
    };

    private static final List<ImageType> imageTypes = Lists.newArrayList();
    static{
        imageTypes.add(ImageType.builder().name("2k_face").size("2048").ranges(new String[]{"0", "511", "1023", "1535"}).build());
        imageTypes.add(ImageType.builder().name("1k_face").size("1024").ranges(new String[]{"0", "511"}).build());
        imageTypes.add(ImageType.builder().name("512_face").size("512").ranges(new String[]{"0"}).build());
    }

    @Value("${upload.type:oss}")
    private String uploadType;
    @Value("${download.config.server-url}")
    private String serverUrl;
    @Value("${download.config.resource-url}")
    private String resourceUrl;
    @Value("${download.config.public-url}")
    private String publicUrl;
    @Value("${download.config.exe-name}")
    private String exeName;
    @Value("${download.config.exe-content}")
    private String exeContent;
    @Autowired
    RestTemplate restTemplate;

    @Autowired
    RedisUtil redisUtil;
    @Autowired
    UploadToOssUtil uploadToOssUtil;



    @Async("sceneDownLoadExecutror")
    public void download(){

            StorageType storageType = StorageType.get(uploadType);
            switch (storageType){
                case OSS:
                    this.download4Ali();
                    break;
                case AWS:
                    this.download4Aws();
                    break;
            }

    }

    public void download4Ali(){

        //场景码
        String num = null;

        //zip包路径
        String zipPath = null;

        //代码文件路径
        //String v3localPath = "/downloads/v3local/";
        String v3localPath = "F:\\downloads\\v3local\\";

        try {
            Set<String> cacheKeys = new HashSet<>();

            //获取任务队列中队头场景码，如果是空，标识没有场景要下载，则退出程序
            num = this.getTaskSceneNum();
            if(StrUtil.isEmpty(num)){
                return;
            }

            Map<String, List<String>> allFiles = this.getAllFiles(num, v3localPath);
            List<String> ossFilePaths = allFiles.get("ossFilePaths");
            List<String> v3localFilePaths = allFiles.get("v3localFilePaths");

            //key总个数
            int total = ossFilePaths.size() + v3localFilePaths.size();
            int count = 0;
            //定义压缩包
            //            zipPath = "/downloads/scenes/" + num + ".zip";
            zipPath = "F:\\downloads\\scenes\\" + num + ".zip";
            File zipFile = new File(zipPath);
            ZipOutputStream out = new ZipOutputStream(zipFile);

            JSONObject getInfoJson = this.zipGetInfoJson(out, num);
            String resolution = "2k";
            if(getInfoJson.getInt("sceneSource") == 3 || getInfoJson.getInt("sceneSource") == 4){
                resolution = "4k";
            }
            int imagesVersion = -1;
            if(getInfoJson.getInt("imagesVersion") != null){
                imagesVersion = getInfoJson.getInt("imagesVersion");
            }

            //固定文件写入
            count = this.zipLocalFiles(out, v3localFilePaths, v3localPath, num, count, total);

            //oss文件写入
            count = this.zipOssFiles(out, ossFilePaths, num, count, total, resolution, imagesVersion, cacheKeys);

            //写入启动命令
            this.zipBat(out, num);

            out.close();

            //上传压缩包
            String uploadPath = "downloads/scenes/" + num + ".zip";
            uploadToOssUtil.upload(zipPath, uploadPath);

            //更新进度100
            this.updateProgress(new BigDecimal("100"), num, 1002, this.publicUrl + uploadPath);

        }catch (Exception e){
            log.error(ExceptionUtil.stacktraceToString(e));
            //更新进度为下载失败
            this.updateProgress( null, num, 1003, null);
        }finally {
            if(StrUtil.isNotEmpty(num)){
                //删除正在下载任务
                redisUtil.lRemove(RedisKey.SCENE_DOWNLOAD_ING, 1, num);
                //删除本地zip包
//                FileUtils.deleteFile(zipPath);
            }
        }
    }

    private int zipOssFiles(ZipOutputStream out, List<String> ossFilePaths, String num, int count, int total, String resolution, int imagesVersion, Set<String> cacheKeys) throws Exception{
        String imageNumPath = "images" + num;
        for (String filePath : ossFilePaths) {
            if(filePath.contains(imageNumPath + "/panorama/panorama_edit/")){
                //如果是编辑目录，只需要更新进度，不需要放进压缩包
                this.updateProgress(new BigDecimal(++count).divide(new BigDecimal(total), 6, BigDecimal.ROUND_HALF_UP), num, 1000, null);
                continue;
            }else if((filePath.contains(imageNumPath + "/panorama/") && filePath.contains("tiles/" + resolution)) || filePath.contains(imageNumPath + "/tiles/" + resolution + "/")) {
                this.processImage(filePath, out, resolution, imagesVersion, cacheKeys);
            }else{
                this.ProcessFiles(filePath, out, this.wwwroot, cacheKeys);
            }

            //更新进度
            this.updateProgress(new BigDecimal(++count).divide(new BigDecimal(total), 6, BigDecimal.ROUND_HALF_UP),  num, 1000, null);
        }
        return count;
    }

    private int zipLocalFiles(ZipOutputStream out, List<String> v3localFilePaths, String v3localPath, String num, int count, int total) throws Exception{
        for (String v3localFilePath : v3localFilePaths) {
            try (FileInputStream in = new FileInputStream(new File(v3localFilePath));){
                this.zipInputStream(out, v3localFilePath.replace(v3localPath, ""), in);
            }catch (Exception e){
                throw e;
            }
            //更新进度
            this.updateProgress(
                new BigDecimal(++count).divide(new BigDecimal(total), 6, BigDecimal.ROUND_HALF_UP),
                num, 1000, null);
        }
        //写入code.txt
        this.zipBytes(out, "code.txt", num.getBytes());
        return count;
    }

    private void zipBat(ZipOutputStream out, String num) throws Exception{
        String batContent = String.format(this.exeContent, num);
        this.zipBytes(out, exeName, batContent.getBytes());

        //更新进度为90%
        this.updateProgress(new BigDecimal("90"), num, 1001, null);
    }

    private Map<String, List<String>> getAllFiles(String num, String v3localPath) throws Exception{
        //列出oss所有文件路径
        List<String> ossFilePaths = new ArrayList<>();
        for (String prefix : prefixArr) {
            prefix = String.format(prefix, num);
            List<String> keys = uploadToOssUtil.listKeys(prefix);
            if(CollUtil.isEmpty(keys)){
                continue;
            }
            if(StorageType.AWS.code().equals(this.uploadType)){
                keys = keys.stream().filter(key->{
                    if(key.contains("x-oss-process")){
                        return false;
                    }
                    return true;
                }).collect(Collectors.toList());
            }
            ossFilePaths.addAll(keys);
        }

        //列出v3local所有文件路径
        File file = new File(v3localPath);
        List<String> v3localFilePaths = FileUtils.list(file);

        HashMap<String, List<String>> map = new HashMap<>();
        map.put("ossFilePaths", ossFilePaths);
        map.put("v3localFilePaths", v3localFilePaths);

        return map;
    }

    private JSONObject zipGetInfoJson(ZipOutputStream out, String num) throws Exception{
        String url = serverUrl + "api/scene/getInfo?num=" + num;
        String getInfoStr = restTemplate.getForObject(url, String.class);
        ResultData resultData = JSONUtil.toBean(getInfoStr, ResultData.class);
        if(resultData == null || ServerCode.SUCCESS.code() != resultData.getCode()){
            throw new Exception("getInfo请求失败，url：" + url);
        }
        JSONObject getInfoJson = JSONUtil.parseObj(resultData.getData());
        getInfoJson.set("sceneScheme", 3);
        getInfoJson.set("needKey", 0);
        getInfoJson.set("sceneKey","");
        //        写入getInfo.json
        String getInfoJsonPath = "wwwroot/data/data"+num + "/getInfo.json";
        this.zipBytes(out, getInfoJsonPath, getInfoJson.toString().getBytes());
        return getInfoJson;
    }

    private String getTaskSceneNum() throws Exception{
        //redis待下载任务出队
        String downloadTask = redisUtil.lLeftPop("downloads:task:v4");
        if(StrUtil.isEmpty(downloadTask)){
            return null;
        }
        DownLoadTaskBean downLoadTaskBean = JSONUtil.toBean(downloadTask, DownLoadTaskBean.class);
        if(downLoadTaskBean == null || StrUtil.isEmpty(downLoadTaskBean.getNum()) || !"local".equals(downLoadTaskBean.getType())){
            throw new Exception("下载任务数据不正确,downloadTask:" +  downloadTask);
        }
        String num = downLoadTaskBean.getNum();
        //正在下载任务入队
        redisUtil.lLeftPush(RedisKey.SCENE_DOWNLOAD_ING, num);
        return num;
    }

    private void processImage(String key, ZipOutputStream out, String resolution, int imagesVersion, Set<String> imgKeys) throws Exception{

        String fileName = key.substring(key.lastIndexOf("/")+1, key.indexOf("."));
        String ext = key.substring(key.lastIndexOf("."));
        String[] arr = fileName.split("_skybox");
        String dir = arr[0];
        String num = arr[1];

        if(StrUtil.isEmpty(fileName)
            || StrUtil.isEmpty(ext)
            || (".jpg".equals(ext) && ".png".equals(ext))
            ||  StrUtil.isEmpty(dir)
            || StrUtil.isEmpty(num)){
            throw new Exception("本地下载图片资源不符合规则，key：" + key);
        }

        for (ImageType imageType : imageTypes) {

            List<ImageTypeDetail> items = Lists.newArrayList();
            String[] ranges = imageType.getRanges();
            for(int i = 0; i < ranges.length; i++){
                String x = ranges[i];
                for(int j = 0; j < ranges.length; j++){
                    String y = ranges[j];
                    items.add(
                        ImageTypeDetail.builder()
                            .i(String.valueOf(i))
                            .j(String.valueOf(j))
                            .x(x)
                            .y(y)
                            .build()
                    );
                }
            }

            for (ImageTypeDetail item : items) {
                String par = "?x-oss-process=image/resize,m_lfit,w_" + imageType.getSize() + "/crop,w_512,h_512,x_" + item.getX() + ",y_" + item.getY();
                if(StorageType.AWS.code().equals(uploadType)){
                    par += "&imagesVersion="+ imagesVersion;
                }

                var url = this.
                    resourceUrl + key;
                StorageType storageType = StorageType.get(uploadType);
                switch (storageType){
                    case OSS:
                        url += par;
                        break;
                    case AWS:
                        url += URLEncoder.encode(par.replace("/", "@"), "UTF-8");
                        break;
                }
                var fky = key.split("/" + resolution + "/")[0] + "/" + dir + "/" + imageType.getName() +  num + "_" + item.getI()  + "_" + item.getJ() + ext;
                if(imgKeys.contains(fky)){
                    continue;
                }
                imgKeys.add(fky);
                byte[] bytes = FileUtils.getBytesFromUrl(url);
                this.zipBytes(out, wwwroot + fky, bytes);
            }

        }

    }

    public void ProcessFiles(String key, ZipOutputStream out, String prefix, Set<String> cacheKeys) throws Exception{
        if(cacheKeys.contains(key)){
            return;
        }
        cacheKeys.add(key);
        String url = this.resourceUrl + key + "?t=" + Calendar.getInstance().getTimeInMillis();
        byte[] data = null;
        if(key.contains("hot.json") || key.contains("link-scene.json")){
            String content = FileUtils.getStringFromUrl(url);

            content.replace(publicUrl, "")
//                .replace(publicUrl+"v3/", "")
                .replace("https://spc.html","spc.html")
                .replace("https://smobile.html", "smobile.html");

            data = content.getBytes();
        }else{
            data = FileUtils.getBytesFromUrl(url);
        }

        zipBytes(out, prefix + key, data);
    }


    public void updateProgress(BigDecimal precent, String num, Integer status, String url){

        if(precent == null){
            precent = new BigDecimal(0);
        }
        if(status == null){
            precent = precent.multiply(new BigDecimal("0.8")).multiply(new BigDecimal("100"));
        }

        DownLoadProgressBean progress = null;
        String key = String.format(RedisKey.PREFIX_DOWNLOAD_PROGRESS, num);
        String progressStr = redisUtil.get(key);
        if(StrUtil.isEmpty(progressStr)){
            progress =  DownLoadProgressBean.builder().percent(precent.intValue()).status(status).url(url).build();
        }else{
            progress = JSONUtil.toBean(progressStr, DownLoadProgressBean.class);
            //如果下载失败，进度不变
            if(status == 1003 && progress.getPercent() != null){
                precent = new BigDecimal(progress.getPercent());
            }
            progress.setPercent(precent.intValue());
            progress.setStatus(status);
            progress.setUrl(url);
        }
        redisUtil.set(key, JSONUtil.toJsonStr(progress));

    }

    public void zipInputStream(ZipOutputStream out, String key, FileInputStream in) throws Exception {
        out.putNextEntry(new org.apache.tools.zip.ZipEntry(key));
        byte[] bytes = new byte[1024];
        int b = 0;
        while ((b = in.read(bytes)) != -1) {
            out.write(bytes, 0, b);
        }
    }

    public void zipBytes(ZipOutputStream out, String key, byte[] bytes) throws Exception {
        out.putNextEntry(new org.apache.tools.zip.ZipEntry(key));
        out.write(bytes);
    }


    public void download4Aws(){


    }




}
