package ru.yandex.webmaster3.storage.util.yt.lock;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.annotations.VisibleForTesting;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import ru.yandex.webmaster3.storage.util.yt.YtCypressService;
import ru.yandex.webmaster3.storage.util.yt.YtCypressServiceImpl;
import ru.yandex.webmaster3.storage.util.yt.YtException;
import ru.yandex.webmaster3.storage.util.yt.YtLockMode;
import ru.yandex.webmaster3.storage.util.yt.YtNode;
import ru.yandex.webmaster3.storage.util.yt.YtNodeAttributes;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtService;

/**
 * ishalaru
 * 10.11.2020
 **/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class CypressProvider {
    private static final String ATTRIBUTE_NODE_MODIFICATION_TIME = "modification_time";
    private static final String ATTRIBUTE_NODE_REVISION = "revision";
    private static final String ATTRIBUTE_SEQ_NUM = "seq_num";

    private static final String ATTR_DATA = "node_info";
    private final YtService ytService;
    @Value("${external.yt.service.locke.root.default}")
    private YtPath root;
    private final EphemeralNodeStateChecker checker;
    private YtCypressService cypressService;

    public void init() {
        cypressService = new YtCypressServiceImpl(ytService, null);
    }

    public CreateBuilder create() {
        return new CreateBuilder(cypressService);
    }

    public CreateBuilder create(YtCypressService cypressService) {
        return new CreateBuilder(cypressService);
    }

    public DeleteBuilder delete() {
        return new DeleteBuilder(cypressService);
    }

    public ChildrenBuilder getChildren() {
        return new ChildrenBuilder(cypressService);
    }

    public GetDataBuilder getData() {
        return new GetDataBuilder(cypressService);
    }

    public GetDataBuilder getData(YtCypressService cypressService) {
        return new GetDataBuilder(cypressService);
    }

    public CheckExistsBuilder checkExists() {
        return new CheckExistsBuilder(cypressService);
    }

    public SetDataBuilder setData() {
        return new SetDataBuilder(cypressService);
    }

    public SetDataBuilder setData(YtCypressService cypressService) {
        return new SetDataBuilder(cypressService);
    }

    public void executeExclusive(String node, Consumer<YtCypressService> consumer) {
        try {
            this.create().forPath(node);
        } catch (YtException exp) {
            //Значит нода уже создана, и создавать его вновь не нужно.
        }
        final YtPath path = YtPath.path(root, node);
        ytService.inTransaction(path).withLock(path, YtLockMode.EXCLUSIVE).execute(e -> {
            consumer.accept(e);
            return true;
        });
    }

    public boolean tryRunExclusive(String node, Duration duration, Consumer<YtCypressService> consumer) {
        return tryRunExclusive(node, duration, Duration.ofMillis(10), consumer);
    }

    public boolean tryRunExclusive(String node, Duration duration, Duration sleepTime, Consumer<YtCypressService> consumer) {
        long waitTime = duration.toMillis();
        long startTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
        while (startTime + waitTime > TimeUnit.NANOSECONDS.toMillis(System.nanoTime())) {
            try {
                executeExclusive(node, consumer);
                log.info("Try run exclusive is completed");
                return true;
            } catch (YtException exp) {
                try {
                    Thread.sleep(sleepTime.toMillis());
                } catch (InterruptedException e) {

                }
            }
        }
        return false;
    }


    public final class CheckExistsBuilder {
        private YtCypressService cypressService;

        public CheckExistsBuilder(YtCypressService cypressService) {
            this.cypressService = cypressService;

        }

        public boolean forPath(String path) {
            final YtPath fullPath = YtPath.path(root, path);
            return this.cypressService.exists(fullPath);
        }
    }

    public final class ChildrenBuilder {
        private YtCypressService cypressService;

        public ChildrenBuilder(YtCypressService cypressService) {
            this.cypressService = cypressService;
        }

        public List<String> forPath(String path) {
            final YtPath fullPath = YtPath.path(root, path);
            return this.cypressService.list(fullPath).stream().map(e -> e.getPathWithoutCluster().substring(fullPath.getPathWithoutCluster().length())).collect(Collectors.toList());
        }
    }

    public final class GetDataBuilder {
        private YtCypressService cypressService;

        private Stat stat;

        public GetDataBuilder(YtCypressService cypressService) {
            this.cypressService = cypressService;
        }

        public GetDataBuilder storingStatIn(Stat stat) {
            this.stat = stat;
            return this;
        }

        public String forPath(String path) {
            final JsonNode nodeMeta = this.cypressService.getNode(YtPath.path(root, path)).getNodeMeta();
            if (stat != null) {
                stat.setModificationTime(nodeMeta.get(ATTRIBUTE_NODE_MODIFICATION_TIME).asLong());
                stat.setVersion(nodeMeta.get(ATTRIBUTE_NODE_REVISION).asLong());
            }

            return nodeMeta != null && nodeMeta.get(ATTR_DATA) != null ? nodeMeta.get(ATTR_DATA).asText() : null;
        }

        public byte[] forPathBinary(String path) {
            return Base64.getDecoder().decode(forPath(path));
        }
    }

    public final class SetDataBuilder {
        private YtCypressService cypressService;
        private long version;

        public SetDataBuilder(YtCypressService cypressService) {
            this.cypressService = cypressService;
            this.version = -1L;
        }

        public SetDataBuilder withVersion(Long version) {
            this.version = version;
            return this;
        }

        public Stat forPath(String path, byte[] data) {
            return forPath(path, Base64.getEncoder().encodeToString(data));
        }

        public Stat forPath(String path, String data) {
            if (version == -1) {
                this.cypressService.set(YtPath.path(YtPath.path(root, path), '@' + ATTR_DATA), TextNode.valueOf(data));
                return createStat(this.cypressService, path);
            } else {
                final YtPath curPath = YtPath.path(root, path);
                Stat stat = new Stat();
                ytService.inTransaction(curPath).withLock(curPath, YtLockMode.EXCLUSIVE).execute(e ->
                {

                    final long version = e.getNode(curPath).getNodeMeta().get(ATTRIBUTE_NODE_REVISION).asLong();
                    if (this.version == version) {
                        e.set(YtPath.path(curPath, '@' + ATTR_DATA), TextNode.valueOf(data));
                    } else {
                        throw new NodeDataUpdateException("Local version not equal to version on the server side.");
                    }
                    Stat tmpStat = createStat(e, path);
                    stat.setVersion(tmpStat.getVersion());
                    stat.setModificationTime(tmpStat.getModificationTime());
                    return true;
                });
                return stat;
            }
        }

        private Stat createStat(YtCypressService service, String path) {
            final JsonNode nodeMeta = service.getNode(YtPath.path(root, path)).getNodeMeta();
            Stat stat = new Stat();
            stat.setModificationTime(nodeMeta.get(ATTRIBUTE_NODE_MODIFICATION_TIME).asLong());
            stat.setVersion(nodeMeta.get(ATTRIBUTE_NODE_REVISION).asLong());
            return stat;
        }
    }

    public final class DeleteBuilder {
        private YtCypressService cypressService;
        private boolean deleteChildrenIfNeeded;


        public DeleteBuilder(YtCypressService cypressService) {
            this.cypressService = cypressService;
            deleteChildrenIfNeeded = false;
        }

        public DeleteBuilder deletingChildrenIfNeeded() {
            this.deleteChildrenIfNeeded = true;
            return this;
        }

        public void forPath(String path) {
            cypressService.remove(YtPath.path(root, path), deleteChildrenIfNeeded);
        }
    }

    public final class CreateBuilder {
        private YtCypressService cypressService;
        private CreateMode mode;
        private boolean ignoreExists;

        public CreateBuilder(YtCypressService cypressService) {
            this.cypressService = cypressService;
            this.mode = CreateMode.PERSISTENT;
            this.ignoreExists = false;
        }

        public CreateBuilder setMode(CreateMode mode) {
            this.mode = mode;
            return this;
        }

        public CreateBuilder ignoreExists(boolean force) {
            this.ignoreExists = ignoreExists;
            return this;
        }

        public String forPath(String path) {
            return this.forPath(path, (String) null);
        }

        public String forPath(String path, byte[] data) {
            return forPath(path, Base64.getEncoder().encodeToString(data));
        }

        public String forPath(String path, String data) {
            YtNodeAttributes attributes = YtNodeAttributes.create(ATTR_DATA, data);
            YtNode ytNode = null;

            if (mode.isSequential()) {
                ytNode = createSequentialNode(path, attributes);
            } else {
                ytNode = cypressService.create(YtPath.path(root, path), YtNode.NodeType.MAP_NODE, true, attributes, mode.isEphemeral() || ignoreExists);
            }
            if (mode.isEphemeral()) {
                checker.add(ytNode.getPath());
            }
            return ytNode.getPath().toString();
        }

        private YtNode createSequentialNode(String path, YtNodeAttributes attributes) {
            for (int i = 0; i < 5; i++) {
                try {
                    List<YtNode> container = new ArrayList<>(1);
                    final YtPath parent = YtPath.path(root, path).getParent();
                    if (!cypressService.exists(parent)) {
                        cypressService.create(parent, YtNode.NodeType.MAP_NODE, true);
                    }
                    ytService.inTransaction(parent).withLock(parent, YtLockMode.EXCLUSIVE).execute(e ->
                    {
                        final JsonNode nodeMeta = e.getNode(parent).getNodeMeta();
                        Long value = 1L;
                        if (nodeMeta.has(ATTRIBUTE_SEQ_NUM)) {
                            value = nodeMeta.get(ATTRIBUTE_SEQ_NUM).asLong();
                        }
                        e.set(YtPath.path(parent, '@' + ATTRIBUTE_SEQ_NUM), LongNode.valueOf(value + 1));
                        final YtNode ytNode = e.create(YtPath.path(root, path + value.toString()), YtNode.NodeType.MAP_NODE, true, attributes);
                        container.add(ytNode);
                        return true;
                    });
                    if (!container.isEmpty()) {
                        return container.get(0);
                    }
                } catch (YtException exp) {
                    log.error(exp.getMessage(), exp);
                }
            }
            throw new YtException("Can't create SequentialNode");
        }
    }

    @VisibleForTesting
    public void setRoot(YtPath root) {
        this.root = root;
    }

    @Data
    @NoArgsConstructor
    public static final class Stat {
        private Long version;
        private Long modificationTime;
    }

    public enum CreateMode {
        PERSISTENT(1, false, false),
        PERSISTENT_SEQUENTIAL(2, false, true),
        EPHEMERAL(3, true, false),
        EPHEMERAL_SEQUENTIAL(2, true, true);
        private boolean ephemeral;
        private boolean sequential;
        private int flag;

        CreateMode(int flag, boolean ephemeral, boolean sequential) {
            this.flag = flag;
            this.ephemeral = ephemeral;
            this.sequential = sequential;
        }

        public boolean isEphemeral() {
            return ephemeral;
        }

        public boolean isSequential() {
            return sequential;
        }

        public int toFlag() {
            return flag;
        }
    }

}
