package cn.com.qmth.examcloud.tool.service.batch_import_exam_student;

import cn.com.qmth.examcloud.tool.cache.LoginSessionManager;
import cn.com.qmth.examcloud.tool.config.SysProperty;
import cn.com.qmth.examcloud.tool.entity.TaskEntity;
import cn.com.qmth.examcloud.tool.service.CommonService;
import cn.com.qmth.examcloud.tool.service.TaskService;
import cn.com.qmth.examcloud.tool.service.batch_import_exam_student.vo.ExamStudentInfo;
import cn.com.qmth.examcloud.tool.service.batch_import_exam_student.vo.ExamStudentInfoListener;
import cn.com.qmth.examcloud.tool.utils.*;
import cn.com.qmth.examcloud.tool.vo.user.User;
import com.alibaba.excel.EasyExcel;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

@Component
public class BatchImportExamStudentTask implements TaskService {

    private final static Logger log = LoggerFactory.getLogger(BatchImportExamStudentTask.class);

    private ExecutorService threadService = Executors.newCachedThreadPool();

    @Autowired
    private SysProperty sysProperty;

    @Autowired
    private CommonService commonService;

    @Override
    public void start(TaskEntity task) {
        User loginUser = LoginSessionManager.getLoginSession(task.getUserToken());
        if (loginUser == null) {
            throw new StatusException("用户未登录！");
        }

        final String tempDir = "task_" + task.getId();
        FileHelper.makeDirs(sysProperty.getDataDir() + "/" + tempDir);

        this.execute(loginUser, task, tempDir);

        // 压缩文件
        FileHelper.zip(new File(sysProperty.getDataDir() + "/" + tempDir),
                new File(sysProperty.getDataDir() + "/" + tempDir + ".zip"));
        task.setFilePath(tempDir + ".zip");
    }

    private void execute(User loginUser, TaskEntity task, String tempDir) {
        final String logFilePath = tempDir + "/task_log_" + task.getId() + ".txt";
        File logFile = new File(sysProperty.getDataDir() + "/" + logFilePath);
        try {
            if (logFile.exists()) {
                //若存在时则先清除旧内容
                FileUtils.deleteQuietly(logFile);
            }
            logFile.createNewFile();
        } catch (IOException e) {
            //ignore
        }

        JsonNode jsonParams = new JsonMapper().getNode(task.getParams());
        if (jsonParams == null) {
            throw new StatusException("任务参数解析错误！");
        }

        Long examId = jsonParams.get("examId").asLong();
        String dataFilePath = jsonParams.get("dataFilePath").asText();
        if (StringUtils.isBlank(dataFilePath)) {
            throw new StatusException("参数dataFilePath不能为空！");
        }

        ExamStudentInfoListener dataListener = new ExamStudentInfoListener();
        try {
            EasyExcel.read(dataFilePath, ExamStudentInfo.class, dataListener).sheet().doRead();
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new StatusException("Excel内容解析错误，请使用标准模板！");
        }

        List<ExamStudentInfo> dataList = dataListener.getList();
        int total = dataList.size();
        log.info("Excel共{}条数据！examId:{} dataFile:{}", total, examId, dataFilePath);
        if (total == 0) {
            throw new StatusException("Excel共0条数据！");
        }
        if (total > 300000) {
            throw new StatusException("Excel的数据限制最大30万条！");
        }

        // 检验数据
        Set<String> courseStudentCodes = new HashSet<>();
        Set<String> courseIdentityNumbers = new HashSet<>();
        Set<String> courseStudentCodeIdentityNumbers = new HashSet<>();
        Map<String, Long> orgMaps = new HashMap<>();
        Map<String, Long> courseMaps = new HashMap<>();
        List<String> errMessages = new ArrayList<>();
        for (int i = 0; i < total; i++) {
            ExamStudentInfo examStudent = dataList.get(i);
            examStudent.setExamId(examId);

            if (StringUtils.isBlank(examStudent.getStudentName())) {
                errMessages.add("第" + (i + 1) + "条 姓名不能为空！");
            } else {
                examStudent.setStudentName(examStudent.getStudentName().trim());
            }

            if (StringUtils.isBlank(examStudent.getStudentCode())) {
                errMessages.add("第" + (i + 1) + "条 学号不能为空！");
            } else {
                examStudent.setStudentCode(examStudent.getStudentCode().trim());
            }

            if (StringUtils.isBlank(examStudent.getIdentityNumber())) {
                errMessages.add("第" + (i + 1) + "条 身份证号不能为空！");
            } else {
                examStudent.setIdentityNumber(examStudent.getIdentityNumber().trim());
            }

            if (StringUtils.isBlank(examStudent.getOrgCode())) {
                errMessages.add("第" + (i + 1) + "条 机构代码不能为空！");
            } else {
                examStudent.setOrgCode(examStudent.getOrgCode().trim());
                Long orgId = orgMaps.get(examStudent.getOrgCode());
                if (orgId == null) {
                    orgId = commonService.queryOrgIdByCode(loginUser, examStudent.getOrgCode());
                    if (orgId == null) {
                        errMessages.add("第" + (i + 1) + "条 机构代码不正确！" + examStudent.getOrgCode());
                    } else {
                        orgMaps.put(examStudent.getOrgCode(), orgId);
                    }
                }
                examStudent.setOrgId(orgId);
            }

            if (StringUtils.isBlank(examStudent.getCourseCode())) {
                errMessages.add("第" + (i + 1) + "条 课程代码不能为空！");
            } else {
                examStudent.setCourseCode(examStudent.getCourseCode().trim());
                Long courseId = courseMaps.get(examStudent.getCourseCode());
                if (courseId == null) {
                    courseId = commonService.queryCourseIdByCode(loginUser, examStudent.getCourseCode());
                    if (courseId == null) {
                        errMessages.add("第" + (i + 1) + "条 课程代码不正确！" + examStudent.getCourseCode());
                    } else {
                        courseMaps.put(examStudent.getCourseCode(), courseId);
                    }
                }
                examStudent.setCourseId(courseId);
            }

            String courseStudentCode = examStudent.getCourseCode() + "@" + examStudent.getStudentCode();
            String courseIdentityNumber = examStudent.getCourseCode() + "@" + examStudent.getIdentityNumber();
            String courseStudentCodeIdentityNumber = courseStudentCode + "@" + examStudent.getIdentityNumber();
            courseStudentCodes.add(courseStudentCode);
            courseIdentityNumbers.add(courseIdentityNumber);
            courseStudentCodeIdentityNumbers.add(courseStudentCodeIdentityNumber);
            // log.info(examStudent.toString());
        }

        if (!errMessages.isEmpty()) {
            FileHelper.writeLines(logFile, errMessages);
            log.warn("检验数据有误！Excel共{}条 examId:{} taskId:{}", total, examId, task.getId());
            task.setDescription("检验数据有误！");
            return;
        }

        log.info("检验数据完成！Excel共{}条 【课程_学号_证件号:{}条】【课程_学号:{}条】【课程_证件号:{}条】 examId:{} taskId:{}",
                total, courseStudentCodeIdentityNumbers.size(), courseStudentCodes.size(), courseIdentityNumbers.size(),
                examId, task.getId());
        // if (true) return;

        // 开始执行导入
        List<String> failRecords = Collections.synchronizedList(new ArrayList<>());
        long startTime = System.currentTimeMillis();
        AtomicInteger successCount = new AtomicInteger(), failCount = new AtomicInteger();
        if (total <= 1000) {
            // 数量少，正常执行
            this.normalRun(loginUser, dataList, successCount, failCount, task.getId(), failRecords);
        } else {
            // 目前多线程并发下会少量执行失败（后端接口目前容易产生事务、数据库约束等错误），针对重复记录也需要拆成多个不重复集合分开分散执行，减少报错情况！！
            List<List<ExamStudentInfo>> newList = this.redoList(dataList);

            // 数量多，多线程分批执行
            final int concurrentSize = 20;
            for (int n = 0; n < newList.size(); n++) {
                List<ExamStudentInfo> list = newList.get(n);
                int cutTotal = list.size();

                for (int start = 0; start < cutTotal; start += concurrentSize) {
                    int end = Math.min(start + concurrentSize, cutTotal);
                    List<ExamStudentInfo> batchList = list.subList(start, end);

                    this.concurrentRun(loginUser, batchList, successCount, failCount, task.getId(), failRecords);

                    long cost = Math.max((System.currentTimeMillis() - startTime) / 1000L, 1);
                    int runCount = successCount.get() + failCount.get();
                    log.info("总数:{} 成功数:{} 失败数:{} 已执行:{}条 已耗时:{}秒 平均每秒{}条 进度:{}% {}"
                            , total, successCount.get(), failCount.get(), runCount, cost, runCount / cost,
                            MathUtils.percentage(runCount, total), predictTime(total, runCount, cost));
                }

                log.info("----------> 分批集合{} size:{}", n + 1, cutTotal);
                try {
                    // 等待几秒后再处理下一步，减少报错情况！！
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    // ignore
                }
            }
        }

        long cost = Math.max((System.currentTimeMillis() - startTime) / 1000L, 1);
        String msg = String.format("导入数据完成！Excel共%s条 【课程_学号_证件号:%s条】【课程_学号:%s条】【课程_证件号:%s条】 成功数:%s 失败数:%s 平均每秒%s条 总耗时:%s秒",
                total, courseStudentCodeIdentityNumbers.size(), courseStudentCodes.size(), courseIdentityNumbers.size(),
                successCount.get(), failCount.get(), total / cost, cost);
        log.info(msg);
        task.setDescription(msg);
        FileHelper.writeLines(logFile, failRecords, true);

        // 汇总错误数据Excel
        List<ExamStudentInfo> errDataList = dataList.stream().filter(ExamStudentInfo::isHasError).collect(Collectors.toList());
        final String errDataPath = sysProperty.getDataDir() + "/" + tempDir + "/task_err_" + task.getId() + ".xlsx";
        List<String> excelHeaders = Lists.newArrayList("姓名", "学号", "身份证号", "机构代码", "机构名称", "课程代码", "课程名称", "专业名称");
        EasyExcel.write(errDataPath).sheet()
                .head(ExcelHelper.buildHeaders(excelHeaders))
                .registerWriteHandler(ExcelHelper.sheetStrategy())
                .registerWriteHandler(ExcelHelper.styleStrategy())
                .doWrite(errDataList);

        dataList.clear();
    }

    private String predictTime(long totalCount, long finishCount, long curCost) {
        if (finishCount == 0 || curCost == 0) {
            return "";
        }
        float nPerSecond = (float) finishCount / curCost;
        float second = (totalCount - finishCount) / nPerSecond;
        return "剩:" + MathUtils.round(second / 60f, 2) + "分钟";
    }

    private void normalRun(User loginUser, List<ExamStudentInfo> batchList, AtomicInteger successCount,
                           AtomicInteger failCount, Long taskId, List<String> failRecords) {
        long startTime = System.currentTimeMillis();
        for (int n = 0; n < batchList.size(); n++) {
            ExamStudentInfo examStudent = batchList.get(n);

            try {
                this.saveExamStudent(loginUser, examStudent);
                successCount.incrementAndGet();
            } catch (Exception e) {
                failCount.incrementAndGet();
                examStudent.setHasError(true);
                String msg = String.format("保存考生失败！examId:%s courseCode:%s identityNumber:%s studentCode:%s %s",
                        examStudent.getExamId(), examStudent.getCourseCode(), examStudent.getIdentityNumber(),
                        examStudent.getStudentCode(), e.getMessage());
                failRecords.add(msg);
                log.error("taskId:{} {}", taskId, msg);
            }

            long cost = Math.max((System.currentTimeMillis() - startTime) / 1000L, 1);
            int runCount = n + 1;
            if (runCount % 20 == 0) {
                log.info("总数:{} 成功数:{} 失败数:{} 已执行:{}条 已耗时:{}秒 平均每秒{}条 进度:{}%"
                        , batchList.size(), successCount.get(), failCount.get(), runCount, cost
                        , runCount / cost, MathUtils.percentage(runCount, batchList.size()));
            }
        }
    }

    private void concurrentRun(User loginUser, List<ExamStudentInfo> batchList, AtomicInteger successCount,
                               AtomicInteger failCount, Long taskId, List<String> failRecords) {
        if (batchList.isEmpty()) {
            return;
        }

        final CountDownLatch commander = new CountDownLatch(1);
        final CountDownLatch worker = new CountDownLatch(batchList.size());

        for (int n = 0; n < batchList.size(); n++) {
            ExamStudentInfo examStudent = batchList.get(n);

            Runnable runnable = () -> {
                try {
                    this.saveExamStudent(loginUser, examStudent);
                    successCount.incrementAndGet();
                } catch (Exception e) {
                    failCount.incrementAndGet();
                    examStudent.setHasError(true);
                    String msg = String.format("保存考生失败！examId:%s courseCode:%s identityNumber:%s studentCode:%s %s",
                            examStudent.getExamId(), examStudent.getCourseCode(), examStudent.getIdentityNumber(),
                            examStudent.getStudentCode(), e.getMessage());
                    failRecords.add(msg);
                    log.error("taskId:{} {}", taskId, msg);
                } finally {
                    worker.countDown();
                }
            };

            threadService.execute(runnable);
        }

        try {
            commander.countDown();
            worker.await();
        } catch (Exception e) {
            log.error("线程异常！{}", e.getMessage());
        }
    }

    private void saveExamStudent(User loginUser, ExamStudentInfo examStudent) {
        Map<String, String> headers = new HashMap<>();
        headers.put("key", loginUser.getKey());
        headers.put("token", loginUser.getToken());

        Map<String, Object> params = new HashMap<>();
        params.put("examId", examStudent.getExamId());
        params.put("courseId", examStudent.getCourseId());
        params.put("orgId", examStudent.getOrgId());
        params.put("studentName", examStudent.getStudentName());
        params.put("studentCode", examStudent.getStudentCode());
        params.put("identityNumber", examStudent.getIdentityNumber());
        params.put("specialtyName", examStudent.getSpecialtyName());
        // 其它字段 暂略...

        String url = loginUser.getServerUrl() + "/api/ecs_exam_work/exam_student";
        String result = HttpHelper.put(url, headers, params);
        // log.info(result);

        JsonNode value = new JsonMapper().getNode(result);
        if (value == null || !value.has("id")) {
            // 创建成功会返回ID，否则失败
            throw new StatusException(result);
        }
    }

    /**
     * 将某个存在重复“身份证号”的数据集合，拆成多个不重复集合
     */
    private List<List<ExamStudentInfo>> redoList(List<ExamStudentInfo> list) {
        if (CollectionUtils.isEmpty(list)) {
            return new ArrayList<>();
        }

        Map<String, List<ExamStudentInfo>> map = list.stream().collect(Collectors.groupingBy(ExamStudentInfo::getIdentityNumber));
        int maxSize = 0;
        for (List<ExamStudentInfo> values : map.values()) {
            maxSize = Math.max(maxSize, values.size());
        }
        log.info("单个身份证号重复最多次数：{}", maxSize);

        List<List<ExamStudentInfo>> newList = new ArrayList<>();
        for (int n = 0; n < maxSize; n++) {
            newList.add(new ArrayList<>());
        }

        for (Map.Entry<String, List<ExamStudentInfo>> entry : map.entrySet()) {
            List<ExamStudentInfo> values = entry.getValue();
            for (int n = 0; n < values.size(); n++) {
                newList.get(n).add(values.get(n));
            }
        }

        return newList;
    }

}
