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

import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.model.controller.ExperimentBarrierSegment;
import be.iminds.ilabt.jfed.rspec.model.impl.BasicDistributeSshKeypair;
import be.iminds.ilabt.jfed.rspec.model.impl.BasicExecuteAnsiblePlaybook;
import be.iminds.ilabt.jfed.rspec.model.impl.BasicOpenflowDataPath;
import be.iminds.ilabt.jfed.rspec.parser.ExtraXml;
import be.iminds.ilabt.jfed.rspec.parser.RspecParseException;
import be.iminds.ilabt.jfed.rspec.parser.StaxRspecWriter;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.util.*;

public class FXModelRspec implements ModelRspec {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(FXModelRspec.class);

    final List<ExtraXml> extraXml = new ArrayList<>();
    private final ObservableList<ExperimentBarrierSegment> barrierSegments = FXCollections.observableArrayList();
    private final Map<String, String> origNameSpaceInfo = new HashMap<>();

    private final ObservableList<FXRspecNode> nodes = FXCollections.observableArrayList();
    private final ObservableList<FXRspecLink> links = FXCollections.observableArrayList();
    private final ObservableList<BasicOpenflowDataPath> openflowDataPaths = FXCollections.observableArrayList();
    //store BasicOpenflowDataPath as we do nothing special with them
    private final ObservableList<Lease> leases = FXCollections.observableArrayList();
    private final ObservableList<FXRspecChannel> channels = FXCollections.observableArrayList();
    private final ObservableList<FXGeantTestbedType> geantTestbedTypes = FXCollections.observableArrayList();
    private final ObservableList<FXAddressPool> addressPools = FXCollections.observableArrayList();
    private final ObservableList<BasicExecuteAnsiblePlaybook> ansibleCookbooks = FXCollections.observableArrayList();
    private final ObservableList<BasicDistributeSshKeypair> distributeSshKeypairs = FXCollections.observableArrayList();

    private final StringProperty expiresDate = new SimpleStringProperty();


    private final BooleanProperty routableAddressesAvailableUsed = new SimpleBooleanProperty(false);
    private final BooleanProperty routableAddressesConfiguredUsed = new SimpleBooleanProperty(false);
    private final IntegerProperty routableAddressesAvailable = new SimpleIntegerProperty();
    private final IntegerProperty routableAddressesConfigured = new SimpleIntegerProperty();

    private final ObjectProperty<Integer> multiplexFactor = new SimpleObjectProperty<>();

    private String origDefaultNamespace = null;
    private String origSchemaLocation = null;
    private final String rspecType;

    private final ObservableList<UserSpec> sshKeys = FXCollections.observableArrayList();

    private final RspecFactory factory;

    /**
     * Create an empty Rspec
     */
    public FXModelRspec(String rspecType) {
        this.rspecType = rspecType;
        this.factory = RspecFactoryFactory.getRspecFactoryInstance(getModelRspecType());
    }

    @Override
    public ModelRspecType getModelRspecType() {
        return ModelRspecType.FX;
    }

    @Override
    public void setParseData(String schemaLocation, String defaultNamespace, Map<String, String> namespaces) {
        this.origSchemaLocation = schemaLocation;
        this.origDefaultNamespace = defaultNamespace;
        this.origNameSpaceInfo.putAll(namespaces);
    }

    @Override
    public String getSchemaLocation() {
        return origSchemaLocation;
    }

    @Override
    public String getOrigDefaultNamespace() {
        return origDefaultNamespace;
    }

    @Override
    public Map<String, String> getNameSpaceInfo() {
        return origNameSpaceInfo;
    }

    @Override
    public void addNode(RspecNode node) {
        Objects.requireNonNull(node, "may not add null node");
        if (!(node instanceof FXRspecNode))
            throw new IllegalArgumentException("may only add FXRspecNode, not " + node.getClass().getName());

        nodes.add((FXRspecNode) node);
    }

    @Override
    public void addLink(RspecLink link) {
        Objects.requireNonNull(link, "may not add null link");
        if (!(link instanceof FXRspecLink))
            throw new IllegalArgumentException("may only add FXRspecLink, not " + link.getClass().getName());

        links.add((FXRspecLink) link);
    }

    @Override
    public ObservableList<FXRspecNode> getNodes() {
        return nodes;
    }

    public ObservableList<ExperimentBarrierSegment> getBarrierSegments() {
        return barrierSegments;
    }

    public void addBarrierSegment(ExperimentBarrierSegment b) {
        for (ExperimentBarrierSegment bs : barrierSegments)
            if (bs.getOrderNumber() == b.getOrderNumber())
                throw new RuntimeException("Duplicate order " + b.getOrderNumber());
        barrierSegments.add(b);
    }

    @Override
    public ObservableList<FXRspecLink> getLinks() {
        return links;
    }


    @Override
    public void addOpenflowDataPath(OpenflowDataPath dp) {
        //stores BasicOpenflowDataPath as we do nothing special with them
        assert dp instanceof BasicOpenflowDataPath;
        openflowDataPaths.add((BasicOpenflowDataPath) dp);
    }

    @Override
    public List<BasicOpenflowDataPath> getOpenflowDataPaths() {
        //stores BasicOpenflowDataPath as we do nothing special with them
        return openflowDataPaths;
    }

    @Override
    public void addLease(Lease lease) {
        leases.add(lease);
    }

    @Override
    public List<Lease> getLeases() {
        return leases;
    }

    @Override
    public void addChannel(Channel channel) {
        assert channel instanceof FXRspecChannel;
        channels.add((FXRspecChannel) channel);
    }

    public boolean deleteChannel(Channel channel) {
        assert channel instanceof FXRspecChannel;
        return channels.remove(channel);
    }

    @Override
    public ObservableList<FXRspecChannel> getChannels() {
        return channels;
    }

    @Override
    public void addGeantTestbedType(GeantTestbedType gtt) {
        assert gtt instanceof FXGeantTestbedType;
        geantTestbedTypes.add((FXGeantTestbedType) gtt);
    }

    public boolean deleteGeantTestbedType(GeantTestbedType gtt) {
        assert gtt instanceof FXGeantTestbedType;
        return geantTestbedTypes.remove(gtt);
    }

    @Override
    public ObservableList<FXGeantTestbedType> getGeantTestbedTypes() {
        return geantTestbedTypes;
    }

    @Override
    public void addAddressPool(AddressPool addressPool) {
        if (addressPool instanceof FXAddressPool)
            addressPools.add((FXAddressPool) addressPool);
        else
            addressPools.add(new FXAddressPool(addressPool));
    }

    @Override
    public boolean deleteAddressPool(AddressPool addressPool) {
        //noinspection SuspiciousMethodCalls
        return addressPools.remove(addressPool);
    }

    @Override
    public ObservableList<FXAddressPool> getAddressPools() {
        return addressPools;
    }

    @Override
    public FXAddressPool getAddressPoolByClientId(String clientId) {
        return addressPools.stream()
                .filter(addressPool -> Objects.equals(clientId, addressPool.getClientId()))
                .findFirst().orElse(null);

    }

    @Override
    public void addExecuteAnsiblePlaybook(ExecuteAnsiblePlaybook executeAnsiblePlaybook) {
        if (executeAnsiblePlaybook instanceof BasicExecuteAnsiblePlaybook)
            ansibleCookbooks.add((BasicExecuteAnsiblePlaybook) executeAnsiblePlaybook);
        else
            ansibleCookbooks.add(new BasicExecuteAnsiblePlaybook(executeAnsiblePlaybook));
    }

    @Override
    public boolean deleteExecuteAnsiblePlaybook(ExecuteAnsiblePlaybook executeAnsiblePlaybook) {
        //noinspection SuspiciousMethodCalls
        return ansibleCookbooks.remove(executeAnsiblePlaybook);
    }

    @Override
    public List<? extends BasicExecuteAnsiblePlaybook> getExecuteAnsiblePlaybooks() {
        return ansibleCookbooks;
    }

    @Override
    public void addDistributeSshKeypair(DistributeSshKeypair distributeSshKeypair) {
        if (distributeSshKeypair instanceof BasicDistributeSshKeypair)
            distributeSshKeypairs.add((BasicDistributeSshKeypair) distributeSshKeypair);
        else
            distributeSshKeypairs.add(new BasicDistributeSshKeypair(distributeSshKeypair));
    }

    @Override
    public boolean deleteDistributeSshKeypair(DistributeSshKeypair distributeSshKeypair) {
        //noinspection SuspiciousMethodCalls
        return distributeSshKeypairs.remove(distributeSshKeypair);
    }

    @Override
    public List<? extends DistributeSshKeypair> getDistributeSshKeypairs() {
        return distributeSshKeypairs;
    }


    @Override
    public FXRspecNode getNodeByUniqueId(String id) {
        for (FXRspecNode node : nodes)
            if (Objects.equals(node.getUniqueId(), id))
                return node;
        return null;
    }

    @Override
    public FXRspecNode getNodeByClientId(String id) {
        for (FXRspecNode node : nodes)
            if (Objects.equals(id, node.getClientId()))
                return node;
        return null;
    }

    @Override
    public FXRspecNode getNodeByComponentId(String id) {
        for (FXRspecNode node : nodes)
            if (node.getComponentId() != null && Objects.equals(node.getComponentId().toString(), id))
                return node;
        return null;
    }

    @Override
    public FXRspecChannel getChannelByComponentId(GeniUrn urn) {
        return channels.stream().filter(channel -> Objects.equals(channel.getComponentId(), urn)).findAny().orElse(null);
    }


    @Override
    public List<FXRspecNode> getNodesByAuthority(Server authority) {
        assert authority != null;
        List<FXRspecNode> nodesOfAuthority = new ArrayList<>();
        for (FXRspecNode node : nodes) {
            assert node != null;
            if (authority.getDefaultComponentManagerAsGeniUrn().equalsIgnoringSubAuth(node.getComponentManagerId())) {
                nodesOfAuthority.add(node);
            }
        }
        return nodesOfAuthority;
    }

    public ExperimentBarrierSegment getBarrierSegmentById(int id) {
        for (ExperimentBarrierSegment b : barrierSegments) {
            if (b.getOrderNumber() == id)
                return b;
        }
        return null;
    }

    @Override
    public FXRspecLink getLinkByClientId(String id) {
        for (FXRspecLink link : links)
            if (Objects.equals(link.getClientId(), id))
                return link;
        return null;
    }

    @Override
    public FXRspecLink getLinkByUniqueId(String id) {
        for (FXRspecLink link : links)
            if (Objects.equals(link.getUniqueId(), id))
                return link;
        return null;
    }

    @Override
    public void deleteNode(RspecNode node) {
        assert node instanceof FXRspecNode;
        List<RspecLink> linksToDelete = new ArrayList<>();
        List<RspecInterface> ifacesToDelete = new ArrayList<>();
        for (RspecInterface iface : node.getInterfaces()) {
            if (iface.isLinkBound()) {
                RspecLink link = iface.getLink();
                if (link != null && link.getInterfaces().size() <= 2)
                    linksToDelete.add(link);
            }
            ifacesToDelete.add(iface);
        }
        ifacesToDelete.forEach(be.iminds.ilabt.jfed.rspec.model.RspecInterface::delete);
        nodes.remove(node);
        linksToDelete.forEach(this::deleteLink);

        for (ExperimentBarrierSegment b : barrierSegments) {
            b.getCommands().stream()
                    .filter(c -> Objects.equals(c.getNodeUniqueID(), node.getClientId()))
                    .forEach(b::deleteCommand);
        }
    }

    @Override
    public void deleteLink(RspecLink link) {
        assert link instanceof FXRspecLink;
        List<RspecInterface> ifacesToDelete = new ArrayList<>(link.getInterfaces());
        ifacesToDelete.forEach(be.iminds.ilabt.jfed.rspec.model.RspecInterface::delete);
        links.remove(link);
    }

    @Override
    public String toGeni3Rspec(RequestRspecSpecialCases specialCase) {
        StaxRspecWriter staxRspecWriter = new StaxRspecWriter(this);

        switch (specialCase) {
            case NONE: {
                //default staxRspecWriter options are fine
                break;
            }
            case PLE: {
                //already using quick hack to get ple to work in: RspecNode.writePropertiesToGeni3RequestRspec()
                List<String> allowedComponentManagers = new ArrayList<>();
                allowedComponentManagers.add("urn:publicid:IDN+ple:ibbtple+authority+cm");
                //hack for ple: don't add nodes from other componentmanagers
                staxRspecWriter.setComponentManagerFilter(allowedComponentManagers);

                //another hack for ple: ple also doesn't like schemaLocation
                staxRspecWriter.setAddSchemaLocation(false);

                //yet another hack for PLE: add a meaningless, empty <sliver/> element to each node
                staxRspecWriter.setAddEmptySliverToNodeHack(true);
                break;
            }
            case NITOS_NO_E_NODE_B_HACK: {
                staxRspecWriter.setUseNitosENodeBHack(false);
                break;
            }
            default:
                throw new RuntimeException("Special case " + specialCase + " not supported");
        }

        try {
            return staxRspecWriter.call();
        } catch (RspecParseException e) {
            LOG.error("Error converting to Rspec: ", e);
            throw new RuntimeException("Error converting to Rspec: " + e, e);
        }
    }

    @Override
    public String toGeni3Rspec() {
        return toGeni3Rspec(RequestRspecSpecialCases.NONE);
    }

    @Override
    public Date getExpires() throws ParseException {
        if (expiresDate.get() == null) return null;
        return RFC3339Util.iso8601StringToDate(expiresDate.get());
    }

    @Override
    public void setExpires(Date expires) {
        if (expires == null)
            this.expiresDate.setValue(null);
        else
            this.expiresDate.setValue(RFC3339Util.dateToRFC3339String(expires, true/*forceZulu*/)); //must be Z for emulab
    }

    @Override
    public String getExpiresString() {
        return expiresDate.get();
    }

    @Override
    public void setExpires(String expires) {
        this.expiresDate.setValue(expires);

    }

    @Override
    public Integer getRoutableAddressesAvailable() {
        return routableAddressesAvailableUsed.getValue() ? routableAddressesAvailable.getValue() : null;
    }

    @Override
    public Integer getRoutableAddressesConfigured() {
        return routableAddressesConfiguredUsed.getValue() ? routableAddressesConfigured.getValue() : null;
    }

    @Override
    public void setRoutableAddressesAvailable(Integer i) {
        if (i == null) {
            routableAddressesAvailableUsed.set(false);
        } else {
            routableAddressesAvailableUsed.set(true);
            routableAddressesAvailable.set(i);
        }
    }

    @Override
    public void setRoutableAddressesConfigured(Integer i) {
        if (i == null) {
            routableAddressesConfiguredUsed.set(false);
        } else {
            routableAddressesConfiguredUsed.set(true);
            routableAddressesConfigured.set(i);
        }
    }

    @Override
    public FXGeantTestbedType getGeantTestbedTypeByName(String geantTestbedTypeName) {
        return geantTestbedTypes.stream()
                .filter(gtt -> Objects.equals(gtt.getName(), geantTestbedTypeName))
                .findFirst().orElse(null);
    }

    @Override
    /**
     * This gathers all the component_manager_id's mentioned in the RSpec.
     *
     * If a new type is added here, it should also be added to the {@link HasComponentManagerIdsValidator} to prevent
     * invalid Rspecs
     */
    public List<GeniUrn> getAllComponentManagerUrns() {
        Set<GeniUrn> urns = new HashSet<>();

        nodes.stream().map(RspecNode::getComponentManagerId).filter(Objects::nonNull).forEach(urns::add);
        links.stream().flatMap(link -> link.getComponentManagerUrns().stream()).forEach(urns::add);
        channels.stream().map(Channel::getComponentManagerId).forEach(urns::add);
        addressPools.stream().map(AddressPool::getComponentManagerId).forEach(urns::add);

        //make sure that a null value is removed! It shouldn't be there in the first place, but better safe than sorry.
        if (urns.contains(null)) {
            LOG.warn("Detected a null component_manager_id. This rspec is not valid! Doing a workaround now...");
            urns.remove(null);
        }

        return new ArrayList<>(urns);
    }

    @Override
    public ObservableList<UserSpec> getSshKeys() {
        return sshKeys;
    }

    @Override
    public void setSshKeys(List<UserSpec> newKeys) {
        sshKeys.setAll(newKeys);
    }

    @Override
    public String nextLinkName() {
        int c = links.size();
        String res = "link" + c;
        while (getLinkByClientId(res) != null)
            res = "link" + (++c);

        return res;
    }

    @Override
    public String nextNodeName() {
        int c = nodes.size();
        String res = "node" + c;
        while (getNodeByClientId(res) != null)
            res = "node" + (++c);

        return res;
    }

    @Override
    public String getType() {
        return rspecType;
    }

    @Override
    public String nextIfaceName(RspecNode node) {
        int c = node.getInterfaces().size();
        String res = node.getClientId() + ":if" + c;
        while (getInterfaceByClientId(res) != null)
            res = node.getClientId() + ":if" + (++c);

        return res;
    }

    @Override
    public FXRspecInterface getInterfaceByUniqueId(String uniqueId) {
        for (FXRspecNode node : nodes) {
            FXRspecInterface iface = node.getInterfaceByUniqueId(uniqueId);

            if (iface != null)
                return iface;
        }
        return null;
    }

    @Override
    public FXRspecInterface getInterfaceByClientId(String clientId) {
        return nodes.stream()
                .map(node -> node.getInterfaceByClientId(clientId))
                .filter(Objects::nonNull)
                .findFirst().orElse(null);
    }

    @Override
    public Integer getMultiplexFactor() {
        return multiplexFactor.get();
    }

    public ObjectProperty<Integer> multiplexFactorProperty() {
        return multiplexFactor;
    }

    public void setMultiplexFactor(Integer multiplexFactor) {
        this.multiplexFactor.set(multiplexFactor);
    }

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

    @Override
    public void setExtraXml(List<ExtraXml> extraXml) {
        this.extraXml.clear();
        this.extraXml.addAll(extraXml);
    }

    @Override
    public ModelRspec copy() {
        return factory.copyModelRspec(this, null/*new rspec type (request/manifest/...)*/);
    }
}
