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

import cd.casic.plugin.core.register.filter.impl.WebSocketFilter;
import cd.casic.plugin.dataobject.pojo.PluginInfo;
import cd.casic.plugin.function.IPluginProcessor;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.ServletContext;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerContainer;
import jakarta.websocket.server.ServerEndpoint;
import jakarta.websocket.server.ServerEndpointConfig;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.pf4j.util.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description: TODO
 * @author: mianbin
 * @date: 2025/5/22 20:04
 * @version: 1.0
 */
@Slf4j
@RequiredArgsConstructor
public class WebSocketProcessor implements IPluginProcessor {

    private final ApplicationContext mainApplicationContext;

    @Override
    public void initialize() throws Exception {

    }

    @Override
    public void registry(PluginInfo plugin) throws Exception {
        ServerContainer serverContainer = getServerContainer();
        if (serverContainer == null) return;

        List<Class<?>> webSocketClassList = plugin.getGroupClass(WebSocketFilter.GROUP_NAME);
        if (webSocketClassList.isEmpty()) return;
        String pluginId = plugin.getPluginId();
        webSocketClassList.forEach(websocketClass -> {
            ServerEndpoint serverEndpoint = websocketClass.getDeclaredAnnotation(ServerEndpoint.class);
            if (serverEndpoint == null) {
                log.warn("WebSocket class {} doesn't has annotation {}", websocketClass.getName(), ServerEndpoint.class.getName());
                return;
            }
            String sourcePath = serverEndpoint.value();
            if (StringUtils.isNullOrEmpty(sourcePath)) {
                return;
            }
            String processPath = sourcePath;
            if (!processPath.startsWith("/")) {
                processPath = "/".concat(processPath);
            }
            UriTemplate uriTemplate;
            try {
                uriTemplate = new UriTemplate(processPath);
            } catch (DeploymentException e) {
                log.error("Websocket path validate failed.", e);
                return;
            }
            String newWebsocketPath = "/".concat(pluginId).concat(processPath);
            String newWebsocketTemplatePath = "/".concat(pluginId).concat(uriTemplate.getPath());
            Map<String, Object> annotationsUpdater;
            try {
                InvocationHandler invocationHandler = Proxy.getInvocationHandler(serverEndpoint);
                Field field = invocationHandler.getClass().getDeclaredField("memberValues");
                field.setAccessible(true);
                annotationsUpdater = (Map<String, Object>) field.get(invocationHandler);
            } catch (Exception e) {
                log.error("Process and update websocket path '{}' annotation exception.", sourcePath, e);
                return;
            }
            try {
                annotationsUpdater.put("value", newWebsocketPath);
                serverContainer.addEndpoint(websocketClass);
                plugin.getWebSocketPathMap().put(newWebsocketPath, newWebsocketTemplatePath);
                log.info("Succeed to create websocket service for path {}", newWebsocketPath);
            } catch (Exception e) {
                log.error("Create websocket service for websocket class " + websocketClass.getName() + " failed.", e);
            } finally {
                annotationsUpdater.put("value", sourcePath);
            }
        });
    }

    @Override
    public void unRegistry(PluginInfo plugin) throws Exception {
        // 不做操作，直接通过关闭PluginApplicationContext完成注销。
    }


    /**
     * 得到 Tomcat ServerContainer
     *
     * @return ServerContainer
     */
    private ServerContainer getServerContainer() {
        try {
            mainApplicationContext.getBean(ServerEndpointExporter.class);
        } catch (BeansException e) {
            log.debug("The required bean of {} not found, if you want to use plugin websocket, please create it.", ServerEndpointExporter.class.getName());
            return null;
        }
        if (!(mainApplicationContext instanceof WebApplicationContext)) {
            return null;
        }
        WebApplicationContext webApplicationContext = (WebApplicationContext) mainApplicationContext;
        ServletContext servletContext = webApplicationContext.getServletContext();
        if (servletContext == null) {
            log.warn("Servlet context is null.");
            return null;
        }
        Object obj = servletContext.getAttribute("javax.websocket.server.ServerContainer");
        if (!(obj instanceof ServerContainer)) {
            return null;
        }
        return (ServerContainer) obj;
    }

    /**
     * 关闭session
     *
     * @param session       session
     * @param websocketPath websocketPath 路径
     * @return 如果需要关闭并且关闭成功, 则返回true。 否则返回false
     * @throws Exception 关闭异常
     */
    @Deprecated
    private boolean closeSession(Session session, String websocketPath) throws Exception {
        EndpointConfig endpointConfig = (EndpointConfig) session.getClass().getDeclaredField("endpointConfig").get(session);
        ServerEndpointConfig perEndpointConfig = (ServerEndpointConfig) endpointConfig.getClass().getDeclaredField("perEndpointConfig").get(endpointConfig);
        String path = perEndpointConfig.getPath();
        if (path.equals(websocketPath)) {
            session.close();
            log.info("Closed websocket session {} for path {}", session.getId(), websocketPath);
            return true;
        }
        return false;
    }

    /**
     * websocket路径解析类，主要用于处理参数
     */
    @Getter
    private static class UriTemplate {

        private final Map<String, Integer> paramMap = new ConcurrentHashMap<>();
        private final String path;

        private UriTemplate(String path) throws DeploymentException {
            if (StrUtil.isEmpty(path) || !path.startsWith("/") || path.contains("/../") || path.contains("/./") || path.contains("//")) {
                throw new DeploymentException(String.format("The path [%s] is not valid.", path));
            }
            StringBuilder normalized = new StringBuilder(path.length());
            Set<String> paramNames = new HashSet<>();

            // Include empty segments.
            String[] segments = path.split("/", -1);
            int paramCount = 0;

            for (int i = 0; i < segments.length; i++) {
                String segment = segments[i];
                if (segment.isEmpty()) {
                    if (i == 0 || (i == segments.length - 1 && paramCount == 0)) {
                        // Ignore the first empty segment as the path must always
                        // start with '/'
                        // Ending with a '/' is also OK for instances used for
                        // matches but not for parameterised templates.
                        continue;
                    } else {
                        // As per EG discussion, all other empty segments are
                        // invalid
                        throw new DeploymentException(String.format("The path [%s] contains one or more empty segments which is not permitted", path));
                    }
                }
                normalized.append('/');
                if (segment.startsWith("{") && segment.endsWith("}")) {
                    segment = segment.substring(1, segment.length() - 1);
                    normalized.append('{');
                    normalized.append(paramCount++);
                    normalized.append('}');
                    if (!paramNames.add(segment)) {
                        throw new DeploymentException(String.format("The parameter [%s] appears more than once in the path which is not permitted", segment));
                    }
                    paramMap.put(segment, paramCount - 1);
                } else {
                    if (segment.contains("{") || segment.contains("}")) {
                        throw new DeploymentException(String.format("The segment [%s] is not valid in the provided path [%s]", segment, path));
                    }
                    normalized.append(segment);
                }
            }
            this.path = normalized.toString();
        }

    }
}
