package be.iminds.ilabt.jfed.rspec.model.impl;

import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.parser.ExtraXml;
import be.iminds.ilabt.jfed.util.GeniUrn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * BasicRspecNode
 */
public class BasicRspecNode implements RspecNode {
    private final static Logger LOG = LoggerFactory.getLogger(BasicRspecNode.class);

    private final BasicModelRspec modelRspec;

    protected final List<SliverType> sliverTypes = new ArrayList<>();
    final List<BasicRspecInterface> interfaces = new ArrayList<>();
    private String clientId;
    private double editorPosX = -1.0;
    private double editorPosY = -1.0;
    private double flackEditorPosX = -1.0;
    private double flackEditorPosY = -1.0;
    private final List<HardwareType> hardwareTypes = new ArrayList<>();
    private Boolean exclusive;
    private Boolean available;
    private NodeLocation nodeLocation;
    private GeniUrn componentManagerId;
    private GeniUrn componentId;
    private String componentName;
    private GeniUrn sliverId;
    private String hostname;
    private boolean hasRoutableControlIp;
    private final List<String> leaseIdRefs = new ArrayList<>();

    private List<BasicExecuteService> executeServices = new ArrayList<>();
    private List<BasicInstallService> installServices = new ArrayList<>();
    private List<LoginService> loginServices = new ArrayList<>();
    private List<ProxyService> proxyServices = new ArrayList<>();
    private List<BasicAnsibleService> ansibleServices = new ArrayList<>();
    private final List<String> ansibleGroups = new ArrayList<>();
    private final List<ExtraXml> extraServicesXmls = new ArrayList<>();


    private final List<ExtraXml> extraXmls = new ArrayList<>();

    private final Set<EmptyTag> emptyTags = new HashSet<>();

    @Override
    public BasicModelRspec getRspec() {
        return modelRspec;
    }

    /**
     * constuctor without initializing ID is not public.
     */
    private BasicRspecNode() {
        this.modelRspec = null;
    }

    BasicRspecNode(BasicModelRspec modelRspec, String clientId) {
        this.clientId = clientId;
        this.modelRspec = modelRspec;
    }

    /**
     * Copy Constructor
     *
     * @param rspecNode node to be copied
     */
    BasicRspecNode(BasicModelRspec newModelRspec, RspecNode rspecNode, InterfaceCopyMethod interfaceCopyMethod) {
        this.modelRspec = newModelRspec;
        this.clientId = rspecNode.getClientId();
        this.editorPosX = rspecNode.getEditorX();
        this.editorPosY = rspecNode.getEditorY();
        this.flackEditorPosX = rspecNode.getFlackEditorX();
        this.flackEditorPosY = rspecNode.getFlackEditorY();
        this.componentId = rspecNode.getComponentId();
        this.componentManagerId = rspecNode.getComponentManagerId();
        this.componentName = rspecNode.getComponentName();
        this.sliverId = rspecNode.getSliverId();
        this.exclusive = rspecNode.getExclusive();
        this.available = rspecNode.getAvailable();
        this.hostname = rspecNode.getHostName();
        this.nodeLocation = rspecNode.getLocation();
        this.leaseIdRefs.clear();
        this.leaseIdRefs.addAll(rspecNode.getLeaseIdRefs());

        setExecuteServices(rspecNode.getExecuteServices());
        this.hardwareTypes.addAll(rspecNode.getHardwareTypes());
        setInstallServices(rspecNode.getInstallServices());
        this.loginServices.addAll(rspecNode.getLoginServices());
        this.proxyServices.addAll(rspecNode.getProxyServices());
        setAnsibleServices(rspecNode.getAnsibleServices());
        this.ansibleGroups.addAll(rspecNode.getAnsibleGroups());
        this.sliverTypes.addAll(rspecNode.getSliverTypes());
        this.hasRoutableControlIp = rspecNode.getRoutableControlIp();

        this.emptyTags.addAll(rspecNode.getEmptyTags());

        this.extraXmls.addAll(rspecNode.getExtraXml());
        this.extraServicesXmls.addAll(rspecNode.getExtraServicesXml());

        if (Objects.equals(interfaceCopyMethod, InterfaceCopyMethod.CREATE_NEW_INTERFACE)) {
            copyInterfaces(rspecNode);
        }
        if (Objects.equals(interfaceCopyMethod, InterfaceCopyMethod.STEAL_INTERFACE)) {
            assert rspecNode instanceof BasicRspecNode;
            stealInterfaces((BasicRspecNode) rspecNode);
        }
    }

    /**
     * creates a new interfaceId, using any info found in rspecInterface as a basis.
     * It will not use the same id as any existing interface on the node or link bound to rspecInterface,
     * or as rspecInterface if it already has one.
     */
    public static String generateNewInterfaceId(RspecInterface rspecInterface) {
        String id = rspecInterface.getClientId();

        String preDigitName = (rspecInterface.getNode() == null ? "" :
                (rspecInterface.getNode().getClientId() != null ? rspecInterface.getNode().getClientId() + ":" : "")
        ) + "if";
        int number = -1;

        if (id != null) {
            int digitsCount = 0;
            while (Character.isDigit(id.charAt(id.length() - digitsCount - 1)))
                digitsCount++;

            int digitsStartLoc = id.length() - digitsCount;

            number = -1;

            if (digitsCount > 0)
                number = Integer.parseInt(id.substring(digitsStartLoc));

            preDigitName = id.substring(0, digitsStartLoc);
        }

        boolean duplicate = true;
        while (duplicate) {
            number++;
            id = preDigitName + number;

            duplicate = rspecInterface.getRspec().getInterfaceByClientId(id) != null;
        }

        return id;
    }

    public void copyInterfaces(RspecNode rspecNode) {
        for (RspecInterface rspecInterface : rspecNode.getInterfaces()) {
            //clone interface
            BasicRspecInterface res;
            if (rspecNode.getRspec() == getRspec())
                res =
                        new BasicRspecInterface(this,
                                rspecInterface.isLinkBound() ? (BasicRspecLink) rspecInterface.getLink()
                                        : null, generateNewInterfaceId(rspecInterface));
            else
                res =
                        new BasicRspecInterface(this, rspecInterface.getClientId()); //since we copy to new model, just copy interface name
            res.overwritePropertiesWith(rspecInterface, true);
        }
    }

    public void stealInterfaces(BasicRspecNode rspecNode) {
        List<BasicRspecInterface> origIfaces = new ArrayList<>(rspecNode.getInterfaces());
        for (BasicRspecInterface rspecInterface : origIfaces) {
            rspecInterface.moveToNode(this);
        }
    }

    @Override
    public String getClientId() {
        return clientId;
    }

    @Override
    public void setClientId(String value) {
        clientId = value;
    }

    @Override
    public String getUniqueId() {
        return BasicStringRspec.makeUniqueNodeId(
                clientId,
                componentId != null ? componentId.getValue() : null,
                componentManagerId != null ? componentManagerId.getValue() : null
        );
    }

    @Override
    public double getEditorX() {
        return editorPosX;
    }

    @Override
    public void setEditorX(double value) {
        editorPosX = value;
    }

    @Override
    public double getEditorY() {
        return editorPosY;
    }

    @Override
    public void setEditorY(double value) {
        editorPosY = value;
    }

    @Override
    public double getFlackEditorX() {
        return flackEditorPosX;
    }

    @Override
    public void setFlackEditorX(double value) {
        flackEditorPosX = value;
    }

    @Override
    public double getFlackEditorY() {
        return flackEditorPosY;
    }

    @Override
    public void setFlackEditorY(double value) {
        flackEditorPosX = value;
    }

    @Override
    public List<HardwareType> getHardwareTypes() {
        return hardwareTypes;
    }

    @Override
    public Boolean getExclusive() {
        return exclusive;
    }

    @Override
    public void setExclusive(Boolean value) {
        this.exclusive = value;
    }

    @Override
    public Boolean getAvailable() {
        return available;
    }

    @Override
    public void setAvailable(Boolean available) {
        this.available = available;
    }

    @Override
    public void setSliverTypeDiskImage(String diskImageName) {
        if (sliverTypes.size() > 1)
            throw new RuntimeException("The caller did expected this RSpec to have exactly one sliver types. (probably because expected this to be a request rspec). But this has " + sliverTypes.size() + " sliver types.");
        assert sliverTypes.size() == 1;

        SliverTypeBuilder builder = new SliverTypeBuilder(sliverTypes.get(0));

        List<DiskImage> diskImageList = new ArrayList<>();
        if (diskImageName != null)
            diskImageList.add(new DiskImage(diskImageName));
        builder.setDiskImages(diskImageList);

        sliverTypes.clear();
        sliverTypes.add(builder.build());
    }

    @Override
    public String getSliverTypeName() {
        if (sliverTypes.size() > 1)
            throw new RuntimeException("The caller did not expect this RSpec to have multiple sliver types. (it probably expected this to be a request rspec, which does indeed not have multiple sliver_type's)");

        if (sliverTypes.isEmpty()) return null;
        return sliverTypes.get(0).getName();
    }

    @Override
    public void setSliverTypeName(String name) {
        assert sliverTypes.size() <= 1;

        SliverTypeBuilder builder;
        if (sliverTypes.size() == 1)
            builder = new SliverTypeBuilder(sliverTypes.get(0)).name(name);
        else
            builder = new SliverTypeBuilder(name);

        sliverTypes.clear();
        sliverTypes.add(builder.build());
    }

    @Override
    public SliverType getSliverType() {
        if (sliverTypes.isEmpty())
            return null;
        if (sliverTypes.size() > 1)
            throw new RuntimeException("The caller did not expect this RSpec to have multiple sliver types. (it probably expected this to be a request rspec, which does indeed not have multiple sliver_type's)");
        return sliverTypes.get(0);
    }

    @Override
    public void setSliverType(SliverType singleType) {
        sliverTypes.clear();
        if (singleType != null)
            sliverTypes.add(singleType);
    }

    @Override
    public String getOsImageUrn() {
        if (sliverTypes.size() > 1)
            throw new RuntimeException("The getOsImageUrn convenience method does not support multiple sliver_types");
        if (sliverTypes.isEmpty())
            return null;
        SliverType sliverType = sliverTypes.get(0);
        if (sliverType.getDiskImages().size() > 1)
            throw new RuntimeException("The getOsImageUrn convenience method does not support multiple disk_images in the sliver_type");
        if (sliverType.getDiskImages().isEmpty())
            return null;
        return sliverType.getDiskImages().get(0).getName();
    }

    @Override
    public List<SliverType> getSliverTypes() {
        return sliverTypes;
    }

    @Override
    public NodeLocation getLocation() {
        return nodeLocation;
    }

    @Override
    public void setLocation(NodeLocation value) {
        this.nodeLocation = value;
    }

    @Override
    public GeniUrn getComponentManagerId() {
        return componentManagerId;
    }

    @Override
    public void setComponentManagerId(String value) {
        if (value == null) {
            this.componentManagerId = null;
            return;
        }

        GeniUrn geniUrn = GeniUrn.parse(value);

        if (geniUrn != null) {
            this.componentManagerId = geniUrn;
        } else {
            LOG.warn("Could not set componentManagerId to {} as it is not a valid URN!", value);
            this.componentManagerId = null;
        }
    }

    @Override
    public void setComponentManagerId(GeniUrn value) {
        componentManagerId = value;
    }

    @Override
    public GeniUrn getComponentId() {
        return componentId;
    }

    @Override
    public void setComponentId(String value) {
        if (value == null) {
            this.componentId = null;
            return;
        }

        GeniUrn geniUrn = GeniUrn.parse(value);

        if (geniUrn != null) {
            this.componentId = geniUrn;
        } else {
            LOG.warn("Could not set componentId to {} as it is not a valid URN!", value);
            this.componentId = null;
        }
    }

    @Override
    public void setComponentId(GeniUrn value) {
        componentId = value;
    }

    @Override
    public String getComponentName() {
        return componentName;
    }

    @Override
    public void setComponentName(String value) {
        componentName = value;
    }

    @Override
    public GeniUrn getSliverId() {
        return sliverId;
    }

    @Override
    public void setSliverId(GeniUrn sliverId) {
        this.sliverId = sliverId;

    }

    @Override
    public void setSliverId(String sliverId) {
        if (sliverId == null) {
            this.sliverId = null;
            return;
        }

        GeniUrn geniUrn = GeniUrn.parse(sliverId);

        if (geniUrn != null) {
            this.sliverId = geniUrn;
        } else {
            LOG.warn("Could not set sliverId to {} as it is not a valid URN!", sliverId);
            this.sliverId = null;
        }
    }

    @Override
    public String getHostName() {
        return hostname;
    }

    @Override
    public void setHostName(String value) {
        this.hostname = value;
    }

    @Override
    public boolean getRoutableControlIp() {
        return hasRoutableControlIp;
    }

    @Override
    public void setRoutableControlIp(boolean hasRoutableControlIp) {
        this.hasRoutableControlIp = hasRoutableControlIp;
    }

    @Override
    public List<BasicExecuteService> getExecuteServices() {
        return executeServices;
    }

    @Override
    public void setExecuteServices(List<? extends ExecuteService> value) {
        this.executeServices = value.stream()
                .map((Function<ExecuteService, BasicExecuteService>) BasicExecuteService::new)
                .collect(Collectors.toList());
    }

    @Override
    public List<BasicInstallService> getInstallServices() {
        return installServices;
    }

    @Override
    public void setInstallServices(List<? extends InstallService> value) {
        this.installServices = value.stream()
                .map((Function<InstallService, BasicInstallService>) BasicInstallService::new)
                .collect(Collectors.toList());
    }

    @Override
    public void addExecuteService(ExecuteService executeService) {
        if (executeService instanceof BasicExecuteService) {
            this.executeServices.add((BasicExecuteService) executeService);
        } else {
            this.executeServices.add(new BasicExecuteService(executeService));
        }
    }

    @Override
    public void addInstallService(InstallService installService) {
        if (installService instanceof BasicInstallService) {
            this.installServices.add((BasicInstallService) installService);
        } else {
            this.installServices.add(new BasicInstallService(installService));
        }

    }

    @Override
    public List<LoginService> getLoginServices() {
        return loginServices;
    }

    @Override
    public void setLoginServices(List<LoginService> value) {
        this.loginServices = value;
    }

    @Override
    public List<ProxyService> getProxyServices() {
        return proxyServices;
    }

    @Override
    public void setProxyServices(List<ProxyService> value) {
        this.proxyServices = value;
    }

    @Override
    public List<BasicAnsibleService> getAnsibleServices() {
        return ansibleServices;
    }

    @Override
    public void setAnsibleServices(List<? extends AnsibleService> value) {
        this.ansibleServices = value.stream()
                .map((Function<AnsibleService, BasicAnsibleService>) BasicAnsibleService::new)
                .collect(Collectors.toList());
    }

    @Override
    public void addAnsibleService(AnsibleService as) {
        if (as instanceof BasicAnsibleService)
            ansibleServices.add((BasicAnsibleService) as);
        else
            ansibleServices.add(new BasicAnsibleService(as));
    }

    @Override
    public List<BasicRspecInterface> getInterfaces() {
        return interfaces;
    }

    @Override
    public BasicRspecInterface getInterfaceByClientId(String clientId) {
        for (BasicRspecInterface iface : interfaces) {
            if (Objects.equals(iface.getClientId(), clientId))
                return iface;
        }
        return null;
    }

    @Override
    public BasicRspecInterface getInterfaceByUniqueId(String uniqueId) {
        for (BasicRspecInterface iface : interfaces) {
            if (Objects.equals(iface.getUniqueId(), uniqueId))
                return iface;
        }
        return null;
    }

    protected boolean addInterface(RspecInterface iface) {
        assert iface instanceof BasicRspecInterface;
        return interfaces.add((BasicRspecInterface) iface);
    }

    protected boolean removeInterface(RspecInterface iface) {
        assert iface instanceof BasicRspecInterface;
        return interfaces.remove(iface);
    }

    @Override
    public List<BasicRspecLink> getLinks() {
        return Collections.unmodifiableList(interfaces.stream().filter(BasicRspecInterface::isLinkBound)
                .map(BasicRspecInterface::getLink)
                .collect(Collectors.toList()));
    }

    @Override
    public List<String> getLeaseIdRefs() {
        return leaseIdRefs;
    }

    @Override
    public void addLeaseIdRef(String leaseIdRef) {
        this.leaseIdRefs.add(leaseIdRef);
    }


    @Override
    public List<String> getAnsibleGroups() {
        return ansibleGroups;
    }

    @Override
    @Nonnull
    public List<ExtraXml> getExtraXml() {
        return extraXmls;
    }

    @Override
    @Nonnull
    public List<ExtraXml> getExtraServicesXml() {
        return extraServicesXmls;
    }


    @Override
    public void addEmptyTag(EmptyTag e) {
        emptyTags.add(e);
    }

    @Override
    public boolean hasEmptyTag(EmptyTag e) {
        return emptyTags.contains(e);
    }

    @Override
    public Collection<EmptyTag> getEmptyTags() {
        return emptyTags;
    }

    /**
     * replace all properties with the properties of another node
     * this assumes the relevant id's are the same.
     * <p>
     * Useful when merging manifests.
     *
     * @param otherNode the other node, from which the properties have to be used
     */
    @Override
    public void overwritePropertiesWith(RspecNode otherNode, boolean overwriteWithNull) {
        assert Objects.equals(this.getUniqueId(), otherNode.getUniqueId());

        if (overwriteWithNull) {
            this.componentId = otherNode.getComponentId();
            this.componentManagerId = otherNode.getComponentManagerId();
            this.editorPosX = otherNode.getEditorX();
            this.editorPosY = otherNode.getEditorY();
            this.flackEditorPosX = otherNode.getFlackEditorX();
            this.flackEditorPosY = otherNode.getFlackEditorY();
            this.componentName = otherNode.getComponentName();
            this.sliverId = otherNode.getSliverId();
            this.exclusive = otherNode.getExclusive();
            this.available = otherNode.getAvailable();
            this.hostname = otherNode.getHostName();
            this.nodeLocation = otherNode.getLocation();
            this.leaseIdRefs.clear();
            this.leaseIdRefs.addAll(otherNode.getLeaseIdRefs());

            this.executeServices.clear();
            setExecuteServices(otherNode.getExecuteServices());

            this.hardwareTypes.clear();
            this.hardwareTypes.addAll(otherNode.getHardwareTypes());

            this.installServices.clear();
            setInstallServices(otherNode.getInstallServices());

            this.ansibleServices.clear();
            setAnsibleServices(otherNode.getAnsibleServices());

            this.loginServices.clear();
            this.loginServices.addAll(otherNode.getLoginServices());

            this.proxyServices.clear();
            this.proxyServices.addAll(otherNode.getProxyServices());

            this.sliverTypes.clear();
            this.sliverTypes.addAll(otherNode.getSliverTypes());

            this.ansibleGroups.clear();
            this.ansibleGroups.addAll(otherNode.getAnsibleGroups());

            this.extraXmls.clear();
            this.extraXmls.addAll(otherNode.getExtraXml());

            this.extraServicesXmls.clear();
            this.extraServicesXmls.addAll(otherNode.getExtraServicesXml());
        } else {
            if (otherNode.getComponentId() != null) this.componentId = otherNode.getComponentId();
            if (otherNode.getComponentManagerId() != null) this.componentManagerId = otherNode.getComponentManagerId();
            if (otherNode.getEditorX() >= 0) this.editorPosX = otherNode.getEditorX();
            if (otherNode.getEditorY() >= 0) this.editorPosY = otherNode.getEditorY();
            if (otherNode.getFlackEditorX() >= 0) this.flackEditorPosX = otherNode.getFlackEditorX();
            if (otherNode.getFlackEditorY() >= 0) this.flackEditorPosY = otherNode.getFlackEditorY();
            if (otherNode.getComponentName() != null) this.componentName = otherNode.getComponentName();
            if (otherNode.getSliverId() != null) this.sliverId = otherNode.getSliverId();
            if (otherNode.getExclusive() != null) this.exclusive = otherNode.getExclusive();
            if (otherNode.getAvailable() != null) this.available = otherNode.getAvailable();
            if (otherNode.getHostName() != null) this.hostname = otherNode.getHostName();
            if (otherNode.getLocation() != null) this.nodeLocation = otherNode.getLocation();

            addMissingExecuteServices(this.executeServices, otherNode.getExecuteServices());
            addMissing(this.hardwareTypes, otherNode.getHardwareTypes());
            addMissingInstallServices(this.installServices, otherNode.getInstallServices());
            addMissing(this.loginServices, otherNode.getLoginServices());
            addMissing(this.proxyServices, otherNode.getProxyServices());
            addMissing(this.sliverTypes, otherNode.getSliverTypes());
            addMissing(this.extraXmls, otherNode.getExtraXml()); //works due to custom comparison in EventListExtraXml
            addMissing(this.extraServicesXmls, otherNode.getExtraServicesXml()); //works due to custom comparison in EventListExtraXml
        }
    }

    private static <T> void addMissing(List<T> target, List<T> other) {
        other.stream().filter(o -> !target.contains(o)).forEach(target::add);
    }

    private static void addMissingExecuteServices(List<BasicExecuteService> target, List<? extends ExecuteService> other) {
        for (ExecuteService o : other) {
            if (o instanceof BasicExecuteService) {
                if (!target.contains(o)) {
                    target.add((BasicExecuteService) o);
                }
            } else {
                target.add(new BasicExecuteService(o));
            }
        }
    }

    private static void addMissingInstallServices(List<BasicInstallService> target, List<? extends InstallService> other) {
        for (InstallService o : other) {
            if (o instanceof BasicInstallService) {
                if (!target.contains(o)) {
                    target.add((BasicInstallService) o);
                }
            } else
                target.add(new BasicInstallService(o));
        }
    }
}
