package me.qwq.doghouse.service;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Service;
import org.springframework.util.function.ThrowingSupplier;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import me.qwq.doghouse.config.DoghouseProperties;
import me.qwq.doghouse.entity.config.SiteConfig;
import me.qwq.doghouse.pojo.bo.ThemeInfo;
import me.qwq.doghouse.util.Cryptos;
import me.qwq.doghouse.util.OSUtils;

@Slf4j
@Service
public class ThemeService {

    private static final String CLASSPATH_THEME_ROOT = "templates/blog";
    private static final ResourcePatternResolver RESOURCE_PATTERN_RESOLVER = new PathMatchingResourcePatternResolver();
    private volatile List<ThemeInfo> cachedThemes = Collections.emptyList();
    private List<ThemeInfo> internalThemes = new ArrayList<>();
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Autowired
    DoghouseProperties doghouseProperties;
    
    @Autowired
    SiteConfig siteConfig;
    
    @PostConstruct
    public void init() throws IOException {
        loadInternalThemes();
        reloadThemes();
        startWatchThread(); // 启动目录监听
    }
    
    /**
     * 载入自带主题, 在 classpath:templates/blog 下
     * @throws IOException
     */
    private void loadInternalThemes() throws IOException {

        // 匹配所有主题的 config.json
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] configs = resolver.getResources("classpath*:" + CLASSPATH_THEME_ROOT + "/*/config.json");

        for (Resource config : configs) {
            ThemeInfo internal = getThemeInfo(config);
            internalThemes.add(internal);
        }
    }
    
    /**
     * 重载主题, 自带主题不变, 主要增加外部主题
     */
    private void reloadThemes() {
        Path themeRoot = Paths.get(doghouseProperties.getWorkPath(), "themes");
        
        if (!Files.exists(themeRoot)) {
            cachedThemes = Collections.unmodifiableList(internalThemes);
            return;
        }

        List<ThemeInfo> themes = new ArrayList<>();
        try (DirectoryStream<Path> ds = Files.newDirectoryStream(themeRoot)) {
            for (Path dir : ds) {
                if (Files.isDirectory(dir)) {
                    Path config = dir.resolve("config.json");
                    if (Files.exists(config)) {
                        ThemeInfo info = getThemeInfo(config);
                        themes.add(info);
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        cachedThemes = new ArrayList<>();
        cachedThemes.addAll(internalThemes);
        cachedThemes.addAll(themes);
        cachedThemes = Collections.unmodifiableList(cachedThemes);
    }
    
    private static void watch(WatchService ws, Path path) throws IOException {
        path.register(
                ws,
                StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_DELETE,
                StandardWatchEventKinds.ENTRY_MODIFY
        );
    }
    
    /**
     * 对外部主题目录的变化监听
     * @throws IOException 
     */
    private void startWatchThread() throws IOException {
        Path themeRoot = Paths.get(doghouseProperties.getWorkPath(), "themes");

        Files.createDirectories(themeRoot);

        Thread thread = new Thread(() -> {
            try {
                WatchService ws = themeRoot.getFileSystem().newWatchService();
                watch(ws, themeRoot);
                
                // 对当前 themeRoot 下已有的 dir 注册 ws
                Files.list(themeRoot).filter(Files::isDirectory).forEach(path -> {
                    try {
                        watch(ws, path);
                    } catch (IOException e) {
                        log.error("Failed to register watch service for path: {}", path);
                    }
                });

                boolean reload = false;
                while (true) {
                    WatchKey key = ws.take();

                    for (WatchEvent<?> event : key.pollEvents()) {
                        WatchEvent.Kind<?> kind = event.kind();
                        if (kind == StandardWatchEventKinds.OVERFLOW) continue;

                        Path dir = (Path) key.watchable();
                        Path context = (Path) event.context();
                        Path affected = dir.resolve(context);

                        if (Files.isDirectory(affected) && dir.equals(themeRoot)) {
                            // 是 themeRoot 下的文件夹事件, 把这个文件夹变更也放到 ws 里面去
                            // 方便监听该文件夹下的 config.json 变更事件
                            watch(ws, affected);
                            Path config = affected.resolve("config.json");
                            if (Files.exists(config)) {
                                reload = true;
                            }
                        } else if (dir.getParent().equals(themeRoot) && "config.json".equals(context.getFileName().toString())) {
                            reload = true;
                        }
                    }

                    if (reload) {
                        try {
                            reloadThemes();
                            // 正常执行后才会 false
                            reload = false;
                        }
                        catch (Exception e) {
                            // 失败后需要点操作时间
                            try {
                                Thread.sleep(1000L);
                            }
                            catch (Exception e1) {}
                        }
                    }

                    key.reset();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        thread.setDaemon(true);
        thread.setName("ThemeWatchService");
        thread.start();
    }
    
    /**
     * 获取所有主题
     * @return
     */
    public List<ThemeInfo> getAllThemes() {
        return cachedThemes;
    }
    
    /**
     * 获取所有主题，默认主题放在第一个
     * @param currentFirst
     * @return
     */
    public List<ThemeInfo> getAllThemes(boolean currentFirst) {
        if (!currentFirst) return cachedThemes;
        List<ThemeInfo> themes = new ArrayList<>(cachedThemes);
        ThemeInfo currentTheme = getCurrentTheme();
        if (currentTheme != null && themes.get(0) != currentTheme && themes.remove(currentTheme)) {
            themes.add(0, currentTheme);
        }
        return themes;
    }
    
    /**
     * 获取当前主题，若为配置或不存在则取读取到的第一个主题
     * @return
     */
    public ThemeInfo getCurrentTheme() {
        String identity = siteConfig.getThemeIdentity();
        Optional<ThemeInfo> optionalTheme = Optional.ofNullable(getThemeInfo(identity));
        
        return optionalTheme.orElseGet(() -> {
            ThemeInfo defaultTheme = cachedThemes.get(0);
            siteConfig.setThemeIdentity(defaultTheme.getIdentity()).saveOrUpdate();
            return defaultTheme;
        });
    }
    
    /**
     * 根据 identity 获取指定主题
     * @param identity
     * @return
     */
    public ThemeInfo getThemeInfo(String identity) {
        Optional<ThemeInfo> optionalTheme = cachedThemes.stream().filter(theme -> {
            return theme.isInternal() || OSUtils.isCaseSensitiveFS() ? 
                    theme.getIdentity().equals(identity):
                    theme.getIdentity().equalsIgnoreCase(identity);
        }).findAny();
        
        return optionalTheme.orElseGet(() -> null);
    }
    
    /**
     * 将指定 identity 的内部主题复制到外部
     * @param identity
     * @return
     * @throws Exception
     */
    public boolean copyInternalToExternal(String identity) throws Exception {
        ThemeInfo themeInfo = getThemeInfo(identity);
        if (themeInfo == null) throw new NullPointerException("未能找到指定的主题");
        if (!themeInfo.isInternal()) throw new RuntimeException("目标主题不是内部主题");
        
        // 解压
        String folderName = themeInfo.getFolderName();
        // 模板文件
        // classpath:templates/blog/{folderName}
        // file:{workPath}/themes/{folderName}
        extract(
                new ClassPathResource(CLASSPATH_THEME_ROOT + "/" + folderName), 
                Paths.get(doghouseProperties.getWorkPath(), "themes", folderName));
        // 静态文件
        // source: classpath:static/blog/{folderName}
        // target: file:{workPath}/themes/{folderName}/static
        extract(
                new ClassPathResource("static/blog/" + folderName), 
                Paths.get(doghouseProperties.getWorkPath(), "themes", folderName, "static"));
        
        return true;
    }
    
    private static void extract(ClassPathResource classpathResource, Path targetDir) throws IOException {
        
        Resource[] resources = RESOURCE_PATTERN_RESOLVER.getResources("classpath*:" + classpathResource.getPath().toString() + "/**");
        String classpathRoot = classpathResource.getURI().toString() + "/";
        for (Resource res : resources) {
            String path = res.getURL().toString();

            Path target = targetDir.resolve(path.replace(classpathRoot, ""));

            if (res.isReadable() && res.exists() && !res.getURL().toString().endsWith("/")) {
                Files.createDirectories(target.getParent());
                try (InputStream in = res.getInputStream()) {
                    Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
                }
            }
        }
    }

    /**
     * 实例化内部主题
     * @param config 内部主题资源 Resource
     * @throws IOException 
     */
    private ThemeInfo getThemeInfo(Resource config) throws IOException {
        String json = config.getContentAsString(StandardCharsets.UTF_8);
        ThemeInfo themeInfo = objectMapper.readValue(json, ThemeInfo.class);

        // uri:        .../templates/blog/bleaching/config.json
        // folderName: bleaching
        Path path = Paths.get(config.getURI());
        String folderName = path.getParent().getFileName().toString();
        
        themeInfo.setFolderName(folderName);
        themeInfo.setDirectory("templates/blog/" + folderName);
        themeInfo.setTemplateDirectory("blog/" + folderName);
        themeInfo.setInternal(true);
        
        // 图片
        String image = null;
        String imageClasspath = themeInfo.getDirectory() + "/image.png";
        ClassPathResource resource = new ClassPathResource(imageClasspath);
        if (resource.exists()) {
            try {
                image = extractThemeCover(() -> resource.getInputStream());
            }
            catch (Exception e) {
                log.error("Failed to read/write classpath image {} to temp",
                        imageClasspath, e);
            }
        }
        themeInfo.setImage(image);
        
        return themeInfo;
    }
    
    /**
     * 实例化外部主题
     * @param configPath 外部主题绝对路径
     * @throws IOException 
     */
    private ThemeInfo getThemeInfo(Path configPath) throws IOException {
        String json = Files.readString(configPath);
        ThemeInfo themeInfo = objectMapper.readValue(json, ThemeInfo.class);
        
        Path path = configPath.getParent();
        themeInfo.setDirectory(path.toString());
        themeInfo.setTemplateDirectory(themeInfo.getDirectory());
        themeInfo.setFolderName(path.getName(path.getNameCount() - 1).toString());
        themeInfo.setInternal(false);
        
        Path potentialImagePath = path.resolve("image.png");
        String image = null;
        if (Files.exists(potentialImagePath)) {
            try {
                image = extractThemeCover(() -> Files.newInputStream(potentialImagePath));
            }
            catch (Exception e) {
                log.warn("Failed to read/write file-system image {} to temp",
                        potentialImagePath, e);
            }
        }
        themeInfo.setImage(image);

        return themeInfo;
    }
    
    /**
     * 主题图释放
     * @param streamSupplier
     * @return
     * @throws Exception
     */
    private String extractThemeCover(ThrowingSupplier<InputStream> streamSupplier) throws Exception {
        Path tmpPath = Paths.get(doghouseProperties.getWorkPath(), "tmp/themes/themeImages");
        Files.createDirectories(tmpPath);

        String sha256;
        try (InputStream in = streamSupplier.get()) {
            sha256 = Cryptos.sha256Hex(in);
        }

        Path target = tmpPath.resolve(sha256 + ".png");
        try (InputStream in = streamSupplier.get()) {
            Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
        }
        catch (FileAlreadyExistsException e) {}

        return "/tmp/themes/themeImages/" + sha256 + ".png";
    }
}
