package cd.casic.plugin.core.register.config;

import cd.casic.plugin.OpsPluginDescriptor;
import cd.casic.plugin.dataobject.pojo.PluginInfo;
import cd.casic.plugin.function.IPluginProcessor;
import cd.casic.plugin.utils.ConfigFileUtils;
import cd.casic.plugin.utils.PathUtils;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.PluginDescriptor;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.PropertiesPropertySourceLoader;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @description: TODO
 * @author: mianbin
 * @date: 2025/5/22 11:08
 * @version: 1.0
 */
@Slf4j
@Component
public class SpringAutoConfigurationFileProcessor implements IPluginProcessor {
    List<PropertySourceLoader> propertySourceLoaders = new ArrayList<>();

    /**
     * The "active profiles" property name.
     */
    private static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";

    /**
     * The "includes profiles" property name.
     */
    private static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";

    @Override
    public void initialize() throws Exception {
        propertySourceLoaders.add(new YamlPropertySourceLoader());
        propertySourceLoaders.add(new PropertiesPropertySourceLoader());
        log.info("Start to hand the SpringAutoConfiguration Class for plugin");
    }

    @Override
    public void registry(PluginInfo plugin) throws Exception {
        PluginDescriptor descriptor = plugin.getPluginWrapper().getDescriptor();
        ConfigurableEnvironment environment = plugin.getPluginApplicationContext().getEnvironment();
        if (!(descriptor instanceof OpsPluginDescriptor)) {
            return;
        }
        List<Resource> resourcesFromDescriptor = getConfigResourceFromDescriptor(plugin);
        List<PropertySource<?>> propProfiles = getPropProfiles(resourcesFromDescriptor);
        if (ObjectUtils.isEmpty(propProfiles)) {
            return;
        }
        for (PropertySource<?> propertySource : propProfiles) {
            environment.getPropertySources().addLast(propertySource);
        }
        // 发现原始文件中配置的 profiles
        List<Profile> profiles = getProfiles(environment);
        if (!ObjectUtils.isEmpty(profiles)) {
            loadProfilesConfig(plugin.getPluginWrapper().getPluginPath(), (OpsPluginDescriptor) descriptor, environment, profiles);
        }

        ConfigurationPropertiesBindingPostProcessor.register(plugin.getPluginApplicationContext());
    }

    @Override
    public void unRegistry(PluginInfo plugin) throws Exception {
        // 暂时无法处理，目前关闭pluginApplicationContext，但是后陆要处理
    }

    private List<Resource> getConfigResourceFromDescriptor(PluginInfo plugin) {
        log.info("插件 {} 尝试从插件描述中读取配置文件名", plugin.getPluginId());
        PluginDescriptor descriptor = plugin.getPluginWrapper().getDescriptor();
        OpsPluginDescriptor opsPluginDescriptor = (OpsPluginDescriptor) descriptor;
        String configFileName = opsPluginDescriptor.getConfigFileName();
        if (StrUtil.isEmpty(configFileName)) {
            log.info("插件 {} 的插件描述中未设置配置文件名", plugin.getPluginId());
            return Collections.emptyList();
        }

        List<String> configFileActiveNames = getConfigFileActiveNames(configFileName, opsPluginDescriptor.getConfigFileActive());
        log.info("插件{} 的配置文件为 {} {}", plugin.getPluginId(), configFileName, configFileActiveNames);
        Path pluginPath = plugin.getPluginWrapper().getPluginPath();

        List<Resource> configFileResources = configFileActiveNames.stream()
                .map(configFileActiveName -> PathUtils.getYamlPath(pluginPath, configFileActiveName))
                .filter(Objects::nonNull)
                .map(FileSystemResource::new)
                .collect(Collectors.toList());
        configFileResources.add(new FileSystemResource(PathUtils.getYamlPath(pluginPath, configFileName)));
        return configFileResources;
    }

    /**
     * 加载 spring.profiles.active/spring.profiles.include 定义的配置
     * @param environment ConfigurableEnvironment
     * @param profiles 主配置文件中定义的值
     * @throws Exception Exception
     */
    private void loadProfilesConfig(Path pluginPath, OpsPluginDescriptor opsPluginDescriptor,
                                    ConfigurableEnvironment environment, List<Profile> profiles) throws Exception {
        // 解析当前文件名称
        for (Profile profile : profiles) {
            String name = profile.getName();
            String fileName = ConfigFileUtils.joinConfigFileName(opsPluginDescriptor.getConfigFileName(), name);


            Path configFilePath = PathUtils.getYamlPath(pluginPath, fileName);
            FileSystemResource configFileResource = new FileSystemResource(configFilePath);
            if(ObjectUtils.isEmpty(configFileResource)){
                continue;
            }
            List<PropertySource<?>> propProfiles = getPropProfiles(Collections.singletonList(configFileResource));
            if(ObjectUtils.isEmpty(propProfiles)){
                return;
            }
            for (PropertySource<?> propertySource : propProfiles) {
                environment.getPropertySources().addLast(propertySource);
            }
        }
        // 重新设置 ActiveProfiles
        String[] names = profiles.stream()
                .filter((profile) -> profile != null && !profile.isDefaultProfile())
                .map(Profile::getName)
                .toArray(String[]::new);
        environment.setActiveProfiles(names);
    }


    private List<String> getConfigFileActiveNames(String configFileName, List<String> configFileActives) {
        return configFileActives.stream()
                .map(configFileActive -> ConfigFileUtils.joinConfigFileName(configFileName, configFileActive))
                .collect(Collectors.toList());
    }

    private boolean canLoadFileExtension(PropertySourceLoader loader, String name) {
        return Arrays.stream(loader.getFileExtensions())
                .anyMatch((fileExtension) -> StringUtils.endsWithIgnoreCase(name,
                        fileExtension));
    }

    /**
     * 从 Resource 中解析出 PropertySource
     *
     * @param resources resources
     * @return List
     * @throws IOException 加载文件 IOException 异常
     */
    private List<PropertySource<?>> getPropProfiles(List<Resource> resources) throws IOException {
        List<PropertySource<?>> propProfiles = new ArrayList<>();
        if (resources == null || resources.isEmpty()) {
            return propProfiles;
        }
        for (Resource resource : resources) {
            if (resource == null || !resource.exists()) {
                continue;
            }
            String filename = resource.getFilename();
            if (ObjectUtils.isEmpty(filename)) {
                log.error("File name is empty!");
                return null;
            }
            for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) {
                if (!canLoadFileExtension(propertySourceLoader, filename)) {
                    continue;
                }
                log.info("正在从 {} 读取插件配置", filename);
                List<PropertySource<?>> propertySources = propertySourceLoader.load(filename, resource);
                if (ObjectUtils.isEmpty(propertySources)) {
                    continue;
                }
                propProfiles.addAll(propertySources);
            }
        }
        return propProfiles;
    }

    private List<Profile> getProfiles(Environment environment) {
        List<Profile> profiles = new ArrayList<>();
        Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty(environment);
        profiles.addAll(getOtherActiveProfiles(environment, activatedViaProperty));
        profiles.addAll(activatedViaProperty);
        profiles.removeIf(
                (profile) -> (profile != null && profile.isDefaultProfile()));
        return profiles;
    }

    private Set<Profile> getProfilesActivatedViaProperty(Environment environment) {
        if (!environment.containsProperty(ACTIVE_PROFILES_PROPERTY)
                && !environment.containsProperty(INCLUDE_PROFILES_PROPERTY)) {
            return Collections.emptySet();
        }
        Binder binder = Binder.get(environment);
        Set<Profile> activeProfiles = new LinkedHashSet<>();
        activeProfiles.addAll(getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
        activeProfiles.addAll(getProfiles(binder, ACTIVE_PROFILES_PROPERTY));
        return activeProfiles;
    }

    private List<Profile> getOtherActiveProfiles(Environment environment, Set<Profile> activatedViaProperty) {
        return Arrays.stream(environment.getActiveProfiles()).map(Profile::new)
                .filter((profile) -> !activatedViaProperty.contains(profile))
                .collect(Collectors.toList());
    }

    private Set<Profile> getProfiles(Binder binder, String name) {
        return binder.bind(name, String[].class).map(this::asProfileSet)
                .orElse(Collections.emptySet());
    }

    private Set<Profile> asProfileSet(String[] profileNames) {
        List<Profile> profiles = new ArrayList<>();
        for (String profileName : profileNames) {
            profiles.add(new Profile(profileName));
        }
        return new LinkedHashSet<>(profiles);
    }


    @Data
    @AllArgsConstructor
    private static class Profile {
        private String name;

        private boolean defaultProfile;

        Profile(String name) {
            this(name, false);
        }
    }

}
