package com.ruoyi.web.controller.system;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.XML;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.ASSERT;
import com.ruoyi.common.utils.LogUtils;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.service.ISysConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.LineNumberReader;
import java.io.RandomAccessFile;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/qwertyasd123456/log")
public class SysLogController extends BaseController
{
    private static final String TOMCAT_LOGS_PATH_ENV_PREFIX = "TOMCAT_CATALINA_LOG_PATH";

    private static final String TYPE_TOMCAT = "tomcat";
    private static final String TYPE_APPLICATION = "application";

    private static final String SCHEME_LINE = "line";
    private static final String SCHEME_BYTE = "byte";

    @Autowired
    private ISysConfigService iSysConfigService;

    private static class LogFile
    {
        public String        name;
        public String        path;
        public List<LogFile> children;
        public boolean       file;
        public Long       size;
        public Long time;
    }

    // 首先读取数据库配置: TOMCAT_CATALINA_LOG_PATH
    // 其次读取yml: TOMCAT_CATALINA_LOG_PATH
    // 其次读取环境变量: TOMCAT_CATALINA_LOG_PATH_端口
    private String getLogPath_tomcat()
    {
        String path;
        path = iSysConfigService.selectConfigByKey(TOMCAT_LOGS_PATH_ENV_PREFIX);
        if(StringUtils.isEmpty(path))
            path = SpringUtils.getProperty(TOMCAT_LOGS_PATH_ENV_PREFIX);
        if(StringUtils.isEmpty(path))
            path = System.getenv(TOMCAT_LOGS_PATH_ENV_PREFIX + "_" + SpringUtils.getProperty("server.port"));
        return path;
    }

    private String getLogPath_application()
    {
        InputStream inputStream = null;
        try
        {
            ClassPathResource resource = new ClassPathResource("logback.xml");
            inputStream = resource.getInputStream();
            String xml = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
            JSONObject map = XML.toJSONObject(xml);
            JSONObject configuration = map.getJSONObject("configuration");
            JSONArray properties = configuration.getJSONArray("property");
            return properties.stream().filter((x) -> ((JSONObject)x).get("name").equals("log.path")).map((x) -> ((JSONObject)x).get("value").toString()).findFirst().orElse(null);
        }
        catch(Exception e)
        {
            e.printStackTrace();
            throw new RuntimeException("未找到日志路径配置");
        }
        finally
        {
            IoUtil.close(inputStream);
        }
    }

    private String getLogPath(String type)
    {
        String path;
        if(TYPE_TOMCAT.equals(type))
            path = getLogPath_tomcat();
        else
            path = getLogPath_application();

        ASSERT.EXP(StringUtils.isNotEmpty(path), "未能获取到日志存储目录: " + type);
        return path;
    }

    @PreAuthorize("@ss.hasPermi('sys:qwertyasd123456:log')")
    @GetMapping("/logsTree")
    public AjaxResult logsTree(String type)
    {
        checkOperator();
        String logPath = getLogPath(type);

        File file = new File(logPath);
        File[] logs = file.listFiles();
        ASSERT.EXP(Objects.nonNull(logs), "日志目录为空");
        Pattern pattern = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
        Map<String, List<File>> groups = new HashMap<>();
        for(File log : logs)
        {
            String name = log.getName();
            if (!log.isFile() || !extIsLog(name))
                continue;
            Matcher matcher = pattern.matcher(name);
            boolean matches = matcher.find();
            if(matches)
            {
                String group = matcher.toMatchResult().group(0);
                if(!groups.containsKey(group))
                    groups.put(group, new ArrayList<>());
                groups.get(group).add(log);
            }
            else
            {
                if(!groups.containsKey(""))
                    groups.put("", new ArrayList<>());
                groups.get("").add(log);
            }
        }
        List<File> curList = groups.remove("");
        List<LogFile> res = groups.keySet().stream()
                                      .sorted(Comparator.<String, String>comparing((x) -> x).reversed())
                                      .map((x) -> {
                                          LogFile f = new LogFile();
                                          f.file = false;
                                          f.children = groups.get(x).stream().sorted(Comparator.comparing(File::getName)).map((y) -> {
                                              LogFile c = new LogFile();
                                              c.file = true;
                                              c.path = y.getName();
                                              c.name = y.getName();
                                              c.size = y.length();
                                              c.time = y.lastModified();
                                              return c;
                                          }).collect(Collectors.toList());
                                          f.name = x;
                                          f.path = x;
                                          return f;
                                      }).collect(Collectors.toList());
        if(CollectionUtil.isNotEmpty(curList))
        {
            curList.stream().sorted(Comparator.comparing(File::getName))
                    .forEach((x) -> {
                        LogFile c = new LogFile();
                        c.file = true;
                        c.path = x.getName();
                        c.name = x.getName();
                        c.size = x.length();
                        c.time = x.lastModified();
                        res.add(0, c);
                    });
        }
        return AjaxResult.success(res);
    }

    @PreAuthorize("@ss.hasPermi('sys:qwertyasd123456:log')")
    @PostMapping("/**")
    public AjaxResult loadLog(HttpServletResponse response, String keyword, @RequestParam(defaultValue = "1") Integer line, @RequestParam(defaultValue = "50") Integer count, Boolean nu, String scheme, String type)
    {
        checkOperator();
        String path = ServletUtils.getPathVariable();
        if(StringUtils.isEmpty(path))
        {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return AjaxResult.error("Not Found");
        }
        String logPath = getLogPath(type);
        String filePath = logPath + "/" + path;
        File file = new File(filePath);
        if(!file.exists())
        {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return AjaxResult.error("Not Found");
        }
        if(!file.canRead())
        {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return AjaxResult.error("Forbidden");
        }
        AjaxResult result;
        if(SCHEME_BYTE.equals(scheme))
            result = loadLog_byte(file, keyword, line, count, nu);
        else
            result = loadLog_line(file, keyword, line, count, nu);

        return result.put("file", path);
    }

    private AjaxResult loadLog_line(File file, String keyword, Integer line, Integer count, Boolean nu)
    {
        FileReader reader = null;
        BufferedReader bufferedReader = null;
        boolean bNu = null != nu && nu;
        int offsetLine = (line - 1) * count;
        String str = null;
        int total = 0;
        int lines = 0;
        boolean filter = StringUtils.isNotEmpty(keyword);
        try
        {
            reader = new FileReader(file);
            bufferedReader = new BufferedReader(reader);
            StringBuilder sb = new StringBuilder();
            while((str = bufferedReader.readLine()) != null)
            {
                if(filter && !str.contains(keyword))
                    continue;
                total++;
                if(total <= offsetLine)
                    continue;
                if(total - offsetLine <= count)
                {
                    if(bNu)
                        sb.append(total).append(": ");
                    sb.append(str).append("\n");
                    lines++;
                }
            }
            return AjaxResult.success()
                             .data(sb.toString())
                             .put("total", total)
                             .put("start", offsetLine + 1)
                             .put("end", offsetLine + lines)
                             .put("size", file.length())
                             .put("time", file.lastModified())
                    ;
        }
        catch(Exception e)
        {
            e.printStackTrace();
            return AjaxResult.error("读取文件失败");
        }
        finally
        {
            IoUtil.close(bufferedReader);
            IoUtil.close(reader);
        }
    }

    private AjaxResult loadLog_byte(File file, String keyword, Integer line, Integer count, Boolean nu)
    {
        final long length = file.length();
        int offsetBytes = PageUtils.offset(line, count);
        if(offsetBytes >= length)
        {
            return AjaxResult.success()
                             .data("")
                             .put("total", length)
                             .put("start", offsetBytes + 1)
                             .put("end", 0)
                             .put("size", length)
                             .put("time", file.lastModified())
                    ;
        }

        boolean bNu = null != nu && nu;
        int total = 0;
        RandomAccessFile reader = null;
        LineNumberReader bufferedReader = null;
        boolean filter = StringUtils.isNotEmpty(keyword);
        try
        {
            reader = new RandomAccessFile(file, "r");
            reader.seek(offsetBytes);

            byte[] buf = new byte[count];
            int read = reader.read(buf);

            StringBuilder sb = new StringBuilder();
            String str = new String(buf);
            bufferedReader = new LineNumberReader(new StringReader(str));
            while((str = bufferedReader.readLine()) != null)
            {
                if(filter && !str.contains(keyword))
                    continue;
                total++;
                if(bNu)
                    sb.append(total).append(": ");
                sb.append(str).append("\n");
            }
            return AjaxResult.success()
                             .data(sb.toString())
                             .put("total", length)
                             .put("start", offsetBytes + 1)
                             .put("end", offsetBytes + read)
                             .put("size", length)
                             .put("time", file.lastModified())
                    ;
        }
        catch(Exception e)
        {
            e.printStackTrace();
            return AjaxResult.error("读取文件失败");
        }
        finally
        {
            IoUtil.close(bufferedReader);
            IoUtil.close(reader);
        }
    }

    @PreAuthorize("@ss.hasPermi('sys:qwertyasd123456:log')")
    @GetMapping("/download/{type}/**")
    public void downloadLog(@PathVariable String type, HttpServletResponse response) throws IOException
    {
        String path = ServletUtils.getPathVariable();
        if(StringUtils.isEmpty(path))
        {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        String logPath;
        try
        {
            logPath = getLogPath(type);
        }
        catch(Exception e)
        {
            e.printStackTrace();
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        String filePath = logPath + "/" + path;
        File file = new File(filePath);
        if(!file.exists())
        {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        if(!file.canRead())
        {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        if (!extIsLog(path))
        {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        String downloadPath = file.getCanonicalPath();
        String downloadName = file.getName();
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        FileUtils.setAttachmentResponseHeader(response, downloadName);
        FileUtils.writeBytes(downloadPath, response.getOutputStream());
    }

    private boolean extIsLog(String path)
    {
        return (path.endsWith(".log") || path.endsWith(".out") || path.endsWith(".txt"));
    }

    private void checkOperator()
    {
        ASSERT.EXP(getLoginUser().getUser().isAdmin() && ArrayUtil.contains(new String[] {
                "dev", "local", "stage",
        }, SpringUtils.getProperty("spring.profiles.active")),
                "无权操作");
    }
}
