package cn.reghao.tnb.sb.zk.cluster;

import cn.reghao.tnb.sb.zk.model.constant.NodeRole;
import cn.reghao.tnb.sb.zk.model.constant.NodeStatus;
import cn.reghao.tnb.sb.zk.model.po.NodeInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.*;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.Stat;
import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author reghao
 * @date 2023-03-29 11:24:00
 */
@Slf4j
@Component
public class Node {
    private final CuratorFramework client;
    private final String nodeId;
    private List<NodeInfo> nodes = new ArrayList<>();

    public Node() {
        // -Dserver.host=127.0.0.1 -Dserver.port=8010
        nodeId = System.getProperty("server.host") + ":" + System.getProperty("server.port");
        // -Dzk.uri=127.0.0.1:2181
        String zkUri = System.getProperty("zk.uri");

        String connectString = "127.0.0.1:2181";
        client = CuratorFrameworkFactory.builder()
                .connectString(connectString)
                .sessionTimeoutMs(5000)
                .connectionTimeoutMs(3000)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .build();
        client.start();
    }

    //@PostConstruct
    private void init() {
        try {
            Stat stat1 = client.checkExists().forPath(NodePath.all_nodes);
            if (stat1 == null) {
                client.create()
                        .creatingParentsIfNeeded()
                        .withMode(CreateMode.PERSISTENT)
                        .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                        .forPath(NodePath.all_nodes);
            }

            //status = NodeRole.LOOKING.name();
            Stat stat2 = client.checkExists().forPath(NodePath.all_nodes + "/" + nodeId);
            if (stat2 == null) {
                client.create()
                        .withMode(CreateMode.PERSISTENT)
                        .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                        .forPath(NodePath.all_nodes + "/" + nodeId, nodeId.getBytes(StandardCharsets.UTF_8));
            }

            startPathChildrenCache();
            init1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void startPathChildrenCache() throws Exception {
        Stat stat = client.checkExists().forPath(NodePath.live_nodes);
        if (stat == null) {
            client.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.PERSISTENT)
                    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                    .forPath(NodePath.live_nodes);
        }

        PathChildrenCache pathChildrenCache = new PathChildrenCache(client, NodePath.live_nodes, true);
        PathChildrenCacheListener listener = (curator, event) -> {
            PathChildrenCacheEvent.Type type = event.getType();
            switch (type) {
                case CONNECTION_LOST:
                case CONNECTION_SUSPENDED:
                case CONNECTION_RECONNECTED:
                case INITIALIZED:
                case CHILD_ADDED:
                case CHILD_UPDATED:
                case CHILD_REMOVED:
                    log.info("新事件 {}", event.getData());
                    electLeader();
            }
        };
        pathChildrenCache.getListenable().addListener(listener);
        pathChildrenCache.start();
    }

    private void init1() throws Exception {
        client.create()
                .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                .forPath(NodePath.live_nodes + "/leader-", nodeId.getBytes(StandardCharsets.UTF_8));
    }

    public void deleteAll() throws Exception {
        deleteDirectory(NodePath.all_nodes);
        deleteDirectory(NodePath.live_nodes);
        client.delete().forPath(NodePath.leader);
    }

    public void deleteDirectory(String dirPath) throws Exception {
        for (String childPath : client.getChildren().forPath(dirPath)) {
            client.delete().forPath(dirPath + "/" + childPath);
        };
    }

    public void electLeader() throws Exception {
        String leaderNodeId;
        Stat stat = client.checkExists().forPath(NodePath.leader);
        if (stat == null) {
            try {
                client.create()
                        .creatingParentsIfNeeded()
                        .withMode(CreateMode.EPHEMERAL)
                        .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                        .forPath(NodePath.leader, nodeId.getBytes(StandardCharsets.UTF_8));
                leaderNodeId = nodeId;
            } catch (Exception e) {
                if (e instanceof KeeperException.NodeExistsException) {
                    leaderNodeId = getLeader();
                } else {
                    throw e;
                }
            }
        } else {
            leaderNodeId = getLeader();
        }

        log.info("leader id -> {}", leaderNodeId);
        getNodes(leaderNodeId);
    }

    public String getLeader() throws Exception {
        byte[] bytes = client.getData().forPath(NodePath.leader);
        return new String(bytes, StandardCharsets.UTF_8);
    }

    public boolean isLeader() {
        try {
            byte[] bytes = client.getData().forPath(NodePath.leader);
            String leaderId = new String(bytes, StandardCharsets.UTF_8);
            return nodeId.equals(leaderId);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    public List<String> getFollowers() throws Exception {
        Map<String, String> followers = getChildData(NodePath.live_nodes);
        return new ArrayList<>(followers.values());
    }

    private void getNodes(String leaderId) throws Exception {
        Map<String, String> electionChildData = getChildData(NodePath.live_nodes);
        log.info("{}", electionChildData);
        Map<String, NodeInfo> onlineNodes = electionChildData.values().stream()
                .map(id -> {
                    if (id.equalsIgnoreCase(leaderId)) {
                        return new NodeInfo(id, NodeStatus.Online.name(), NodeRole.LEADER.name());
                    } else {
                        return new NodeInfo(id, NodeStatus.Online.name(), NodeRole.FOLLOWER.name());
                    }
                })
                .collect(Collectors.toMap(NodeInfo::getId, Function.identity()));

        getChildData(NodePath.all_nodes).values().forEach(nodeId -> {
            if (nodeId.equals(leaderId)) {
                onlineNodes.put(nodeId, new NodeInfo(nodeId, NodeStatus.Online, NodeRole.LEADER.name()));
            }

            if (!onlineNodes.containsKey(nodeId)) {
                onlineNodes.put(nodeId, new NodeInfo(nodeId, NodeStatus.Offline, null));
            }
        });
        this.nodes = new ArrayList<>(onlineNodes.values());
    }

    private Map<String, String> getChildData(String parent) throws Exception {
        List<String> list = client.getChildren().forPath(parent);
        Map<String, String> map = new TreeMap<>();
        for (String node : list) {
            String path = parent + "/" + node;
            byte[] bytes = client.getData().forPath(path);

            map.put(path, new String(bytes, StandardCharsets.UTF_8));
        }

        return map;
    }

    public List<NodeInfo> getAllNodes() {
        return this.nodes;
    }
}
