package com.njcn.system.service.impl;

import cn.afterturn.easypoi.exception.excel.ExcelImportException;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nimbusds.jose.JWSObject;
import com.njcn.common.config.GeneralInfo;
import com.njcn.common.pojo.constant.SecurityConstants;
import com.njcn.common.pojo.dto.UserTokenInfo;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.oss.constant.OssPath;
import com.njcn.redis.utils.RedisUtil;
import com.njcn.system.enums.AuditLogEnum;
import com.njcn.system.excel.UserLogExcel;
import com.njcn.system.mapper.AuditMapper;
import com.njcn.system.mapper.UserLogMapper;
import com.njcn.system.pojo.param.AuditParam;
import com.njcn.system.pojo.po.UserLog;
import com.njcn.system.pojo.vo.*;
import com.njcn.system.service.AuditService;
import com.njcn.user.api.UserFeignClient;
import com.njcn.user.pojo.po.User;
import lombok.RequiredArgsConstructor;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;

import java.io.*;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @version 1.0.0
 * @author: chenchao
 * @date: 2022/07/11 20:56
 */
@Service
@RequiredArgsConstructor
public class AuditServiceImpl extends ServiceImpl<UserLogMapper, UserLog> implements AuditService {

    private final AuditMapper auditMapper;

    @Value("${spring.datasource.druid.url}")
    private String urls;
    private final GeneralInfo generalInfo;

    private final RedisUtil redisUtil;
    private final UserFeignClient userFeignClient;

    @Override
    public Page<AuditLogVO> getAuditLog(AuditParam auditParam) {
        List<AuditLogVO> auditLogVOS = new ArrayList<>();

        Page<UserLog> info = this.page(new Page<>(auditParam.getPageNum(), auditParam.getPageSize()), new LambdaQueryWrapper<UserLog>()
                .like(StrUtil.isNotBlank(auditParam.getLoginName()), UserLog::getLoginName, auditParam.getLoginName())
                .ne(UserLog::getLoginName, "unknown user")
                .ne(UserLog::getLoginName, "")
                .ne(UserLog::getOperate, "unknown user")
                .eq(auditParam.getType() != null, UserLog::getType, auditParam.getType())
                .eq(StrUtil.isNotBlank(auditParam.getOperateType()), UserLog::getOperateType, auditParam.getOperateType())
                .ge(StrUtil.isNotBlank(auditParam.getSearchBeginTime()), UserLog::getCreateTime, DateUtil.beginOfDay(DateUtil.parse(auditParam.getSearchBeginTime())))
                .le(StrUtil.isNotBlank(auditParam.getSearchEndTime()), UserLog::getCreateTime, DateUtil.endOfDay(DateUtil.parse(auditParam.getSearchEndTime())))
                .or(StrUtil.isNotBlank(auditParam.getLoginName()), qw ->
                        qw.like(StrUtil.isNotBlank(auditParam.getLoginName()), UserLog::getIp, auditParam.getLoginName())
                                .ne(UserLog::getLoginName, "unknown user")
                                .ne(UserLog::getLoginName, "")
                                .ne(UserLog::getOperate, "unknown user")
                                .eq(auditParam.getType() != null, UserLog::getType, auditParam.getType())
                                .eq(StrUtil.isNotBlank(auditParam.getOperateType()), UserLog::getOperateType, auditParam.getOperateType())
                                .ge(StrUtil.isNotBlank(auditParam.getSearchBeginTime()), UserLog::getCreateTime, DateUtil.beginOfDay(DateUtil.parse(auditParam.getSearchBeginTime())))
                                .le(StrUtil.isNotBlank(auditParam.getSearchEndTime()), UserLog::getCreateTime, DateUtil.endOfDay(DateUtil.parse(auditParam.getSearchEndTime())))
                )
                .or(StrUtil.isNotBlank(auditParam.getLoginName()), qw ->
                        qw.like(StrUtil.isNotBlank(auditParam.getLoginName()), UserLog::getCreateTime, auditParam.getLoginName())
                                .ne(UserLog::getLoginName, "unknown user")
                                .ne(UserLog::getLoginName, "")
                                .ne(UserLog::getOperate, "unknown user")
                                .eq(auditParam.getType() != null, UserLog::getType, auditParam.getType())
                                .eq(StrUtil.isNotBlank(auditParam.getOperateType()), UserLog::getOperateType, auditParam.getOperateType())
                                .ge(StrUtil.isNotBlank(auditParam.getSearchBeginTime()), UserLog::getCreateTime, DateUtil.beginOfDay(DateUtil.parse(auditParam.getSearchBeginTime())))
                                .le(StrUtil.isNotBlank(auditParam.getSearchEndTime()), UserLog::getCreateTime, DateUtil.endOfDay(DateUtil.parse(auditParam.getSearchEndTime())))
                )
                .orderByDesc(UserLog::getCreateTime)
        );
        Page<AuditLogVO> page = BeanUtil.copyProperties(info, Page.class);
        if (CollUtil.isNotEmpty(info.getRecords())) {
            for (UserLog userLog : info.getRecords()) {
                AuditLogVO auditLogVO = new AuditLogVO();
                String updateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(userLog.getUpdateTime());
                auditLogVO.setTime(updateTime);
                auditLogVO.setUserName(userLog.getLoginName());
                auditLogVO.setOperate(userLog.getOperate());
                StringBuilder describe = new StringBuilder();
                describe.append(auditLogVO.getUserName()).append("在").append(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(userLog.getUpdateTime())).append("在").append(userLog.getIp()).append("执行了").append(userLog.getOperate()).append("，结果为");
                if (userLog.getResult() == 1) {
                    describe.append("成功");
                }
                if (userLog.getResult() == 0) {
                    describe.append("失败").append("，失败原因为").append(userLog.getFailReason());
                }
                auditLogVO.setDescribe(describe.toString());
                auditLogVO.setType(userLog.getType() == 0 ? "业务事件" : "系统事件");
                auditLogVO.setResult(userLog.getResult() == 0 ? "失败" : "成功");
                auditLogVO.setIp(userLog.getIp());
                if (userLog.getLevel() == 0) {
                    auditLogVO.setLevel("普通");
                }
                if (userLog.getLevel() == 1) {
                    auditLogVO.setLevel("中等");
                }
                if (userLog.getLevel() == 2) {
                    auditLogVO.setLevel("严重");
                }
                auditLogVOS.add(auditLogVO);
            }
        }
        //当前页数据
        page.setRecords(auditLogVOS);
        return page;
    }

    @Override
    public void logFileWriter() {
        //限制多次点击
        String logFileWriter = redisUtil.getStringByKey("logFileWriter");
        if (StrUtil.isNotBlank(logFileWriter) || ObjectUtil.equals(logFileWriter, "1")) {
            throw new BusinessException(AuditLogEnum.MULTIPLE_CLICKS_LOGFILEWRITER);
        }
        redisUtil.saveByKey("logFileWriter", "1");
        LocalDateTime nowDate = LocalDateTime.now();
        LocalDateTime agoDate = nowDate.minusMonths(6).with(TemporalAdjusters.firstDayOfMonth());
        String date = agoDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
        //结束时间
        String endTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH_mm_ss").format(nowDate);
        //文件名
        QueryWrapper<UserLog> aa = new QueryWrapper<>();
        aa.select("id");
        aa.ge("sys_user_log.update_time", date);
        aa.le("sys_user_log.update_time", endTime);
        String nowTime = date + "至" + endTime;
        File file = new File(generalInfo.getBusinessTempPath() + File.separator + OssPath.LOGBAK);
        if (!file.exists() && !file.isDirectory()) {
            file.mkdir();
        }
        //必须放到循环外，否则会刷新流
        ExcelWriter excelWriter = EasyExcel.write(generalInfo.getBusinessTempPath() + File.separator + OssPath.LOGBAK + nowTime + ExcelTypeEnum.XLSX.getValue(), UserLogExcel.class)
//                .excelType(ExcelTypeEnum.CSV)
                .build();

        try {
            //记录总数:实际中需要根据查询条件进行统计即可:一共多少条
            int totalCount = this.count(aa);
            //每一个Sheet存放100w条数据
            Integer sheetDataRows = 1000000;
            //每次写入的数据量20w,每页查询20W
            Integer writeDataRows = 200000;
            //计算需要的Sheet总数量
            Integer sheetNum = totalCount % sheetDataRows == 0 ? (totalCount / sheetDataRows) : (totalCount / sheetDataRows + 1);
            //计算一般情况下每一个Sheet需要写入的次数(一般情况不包含最后一个sheet,因为最后一个sheet不确定会写入多少条数据)
            Integer oneSheetWriteCount = sheetDataRows / writeDataRows;
            //计算最后一个sheet需要写入的次数
            Integer lastSheetWriteCount = 0;
            if (totalCount > sheetDataRows) {
                lastSheetWriteCount = totalCount % sheetDataRows == 0 ? oneSheetWriteCount : (totalCount % sheetDataRows % writeDataRows == 0 ? (totalCount / sheetDataRows / writeDataRows) : (totalCount / sheetDataRows / writeDataRows + 1));
            } else {
                lastSheetWriteCount = totalCount % sheetDataRows == 0 ? oneSheetWriteCount : oneSheetWriteCount + 1;
            }

            //开始分批查询分次写入
            for (int i = 0; i < sheetNum; i++) {

                //循环写入次数: j的自增条件是当不是最后一个Sheet的时候写入次数为正常的每个Sheet写入的次数,如果是最后一个就需要使用计算的次数lastSheetWriteCount
                for (int j = 0; j < (i != sheetNum - 1 ? oneSheetWriteCount : lastSheetWriteCount); j++) {
                    //分页查询一次20w
                    //查询分页列表---按照自己的业务查列表，分页这个一定要使用这个：page1.getPageNum(),page1.getPageSize()！！！
                    Page<UserLogExcel> userLogExcelPage = auditMapper.queryExportLimit(new Page<>(j + 1 + oneSheetWriteCount * i, writeDataRows), date, endTime);
                    List<UserLogExcel> list = userLogExcelPage.getRecords();
                    //写入到excel:
                    WriteSheet writeSheet = EasyExcel.writerSheet(i, "Sheet" + (i + 1)).head(UserLogExcel.class)
                            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
                    excelWriter.write(list, writeSheet);
                }
            }

        } catch (Exception e) {
            redisUtil.delete("recoverLogFile");
            e.printStackTrace();
        } finally {
            excelWriter.finish();
        }
        redisUtil.delete("logFileWriter");
    }


    public static MultipartFile getMultipartFile(File file) {
        FileItem item = new DiskFileItemFactory().createItem("file"
                , MediaType.MULTIPART_FORM_DATA_VALUE
                , true
                , file.getName());
        try (InputStream input = new FileInputStream(file);
             OutputStream os = item.getOutputStream()) {
            // 流转移
            IOUtils.copy(input, os);
        } catch (Exception e) {
            throw new IllegalArgumentException("Invalid file: " + e, e);
        }
        return new CommonsMultipartFile(item);
    }

    @Override
    @Transactional(rollbackFor = {Exception.class})
    public void recoverLogFile() {
        String logFileWriter = redisUtil.getStringByKey("recoverLogFile");
        if (StrUtil.isNotBlank(logFileWriter) || ObjectUtil.equals(logFileWriter, "1")) {
            throw new BusinessException(AuditLogEnum.MULTIPLE_CLICKS_LOGFILEWRITER);
        }
        redisUtil.saveByKey("recoverLogFile", "1");
        File newFile = getLastFile();
        if (FileUtil.isEmpty(newFile)) {
            throw new BusinessException(AuditLogEnum.NOT_FIND_FILE);
        }
        FileInputStream in = null;
        try {
            in = new FileInputStream(newFile);
            Set<String> logIndex = new HashSet<>(this.baseMapper.getAllIndex());
            List<UserLog> userLogs = new ArrayList<>();
            EasyExcel.read(newFile.getPath(), UserLog.class, new AnalysisEventListener<UserLog>() {
                @Override
                public void invoke(UserLog userLog, AnalysisContext analysisContext) {
                    // 将读取到的每一行存入reportDetails集合中
                    if (!logIndex.contains(userLog.getId())) {
                        userLogs.add(userLog);
                    }
                }

                @Override
                public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                }
            }).sheet().doRead();
            if(CollUtil.isNotEmpty(userLogs)){
                // 执行批量插入
                saveLogsBatch(userLogs, 1500);
            }
        } catch (Exception e) {
            redisUtil.delete("recoverLogFile");
            e.printStackTrace();
            throw new ExcelImportException(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(in);
        }
        redisUtil.delete("recoverLogFile");
    }


    /***
     * 批量插入日志
     * @author hongawen
     * @date 2023/5/30 15:56
     * @return boolean
     */
    public boolean saveLogsBatch(Collection<UserLog> userLogs, int batchSize) {
        try {
            int size = userLogs.size();
            int idxLimit = Math.min(batchSize, size);
            List<UserLog> userLogTemp = new ArrayList<>(userLogs);
            //保存单批提交的数据集合
            if (idxLimit == batchSize) {
                int times = size / idxLimit + 1;
                for (int i = 1; i <= times; i++) {
                    if (size > idxLimit) {
                        List<UserLog> temp = userLogTemp.subList(0, idxLimit);
                        this.baseMapper.insertBatchSomeColumn(temp);
                        temp.clear();
                        size = size - idxLimit;
                    } else {
                        this.baseMapper.insertBatchSomeColumn(userLogTemp);
                    }
                }
            } else {
                this.baseMapper.insertBatchSomeColumn(userLogTemp);
            }
        } catch (Exception e) {
            log.error("saveBatch fail", e);
            return false;
        }
        return true;
    }


    @Override
    public Page<AuditLogCusVO> censusAuditLog(AuditParam auditParam) {
        auditParam.setSearchBeginTime(DateUtil.beginOfDay(DateUtil.parse(auditParam.getSearchBeginTime())).toString());
        auditParam.setSearchEndTime(DateUtil.endOfDay(DateUtil.parse(auditParam.getSearchBeginTime())).toString());
        //待分页数据总量
        Page<AuditLogCusVO> page = auditMapper.selectCensusAuditLog(new Page<>(auditParam.getPageNum(), auditParam.getPageSize()), auditParam);
        return page;
    }

    @Override
    public void clearHistoryLog() {
        LocalDate nowDate = LocalDate.now();
        LocalDate agoDate = nowDate.minusMonths(6).with(TemporalAdjusters.firstDayOfMonth());
        String date = agoDate.format(DateTimeFormatter.ISO_LOCAL_DATE);

        QueryWrapper<UserLog> queryWrapper = new QueryWrapper<>();
        queryWrapper.lt("sys_user_log.create_time", date);
        auditMapper.deleteByTime(queryWrapper);
    }

    @Override
    public Float getMemoInfo() {
        String schema = urls.substring(urls.lastIndexOf("/") + 1, urls.lastIndexOf("?"));
        return this.baseMapper.getMemoInfo(schema);
    }

    @Override
    public List<OnlineUsersVO> getOnlineUsers() throws ParseException {
        List<OnlineUsersVO> info = new ArrayList<>();
        List<String> ids = new ArrayList<>();
        Map<String, Long> time = new HashMap<>();
        List<UserTokenInfo> listAllValues = (List<UserTokenInfo>) redisUtil.getLikeListAllValues(SecurityConstants.TOKEN_ONLINE_PREFIX);
        for (UserTokenInfo listAllValue : listAllValues) {
            String payload = StrUtil.toString(JWSObject.parse(listAllValue.getRefreshToken()).getPayload());
            cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(payload);
            time.put(jsonObject.getStr(SecurityConstants.USER_INDEX_KEY), Long.valueOf(jsonObject.getStr("exp")));
            ids.add(jsonObject.getStr(SecurityConstants.USER_INDEX_KEY));
        }

        List<User> data = userFeignClient.getUserByIdList(ids).getData();
        OnlineUsersVO vo;
        if (CollUtil.isNotEmpty(data)) {
            for (User datum : data) {
                vo = BeanUtil.copyProperties(datum, OnlineUsersVO.class);
                Long integer = time.get(datum.getId());
                DateTime date = DateUtil.date(integer * 1000);
                vo.setTime(date.toDateStr());
                info.add(vo);
            }
        }
        return info;
    }

    @Override
    public LogParamVO saveLogParam() {
        LogParamVO logParamVO = new LogParamVO();
        List<String> loginName = auditMapper.selectLoginName();
        List<String> operateType = auditMapper.selectOperateType();
        List<ValuePO> loginNameList = new ArrayList<>();
        List<ValuePO> operateTypeList = new ArrayList<>();
        for (int i = 0; i < loginName.size(); i++) {
            ValuePO valuePO = new ValuePO();
            valuePO.setName("选项" + (i + 1));
            valuePO.setValue(loginName.get(i));
            loginNameList.add(valuePO);
        }
        for (int i = 0; i < operateType.size(); i++) {
            ValuePO valuePO = new ValuePO();
            valuePO.setName("选项" + (i + 1));
            valuePO.setValue(operateType.get(i));
            operateTypeList.add(valuePO);
        }
        logParamVO.setLoginName(loginNameList);
        logParamVO.setOperateType(operateTypeList);
        return logParamVO;
    }

    /**
     * 返回一个最新修改日期的文件
     */
    public File getLastFile() {
        File parentFile = new File(generalInfo.getBusinessTempPath() + File.separator + OssPath.LOGBAK);

        //文件夹下的所有子文件数组
        File[] files = parentFile.listFiles();
        List<Date> list = Arrays.stream(files).map(tempFile -> FileUtil.lastModifiedTime(tempFile)).collect(Collectors.toList());
        //Date集合排序
        Collections.sort(list, Comparator.reverseOrder());
        File newFile = Arrays.stream(files).filter(tempFile -> FileUtil.lastModifiedTime(tempFile).equals(list.get(0))).collect(Collectors.toList()).get(0);

        return newFile;
    }

}
