package be.iminds.ilabt.jfed.rspec.model.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.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 org.slf4j.LoggerFactory;

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

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

    private final List<ExtraXml> extraXml = new ArrayList<>();
    private final Map<String, String> origNameSpaceInfo = new HashMap<>();

    private final List<BasicRspecNode> nodes = new ArrayList<>();
    private final List<BasicRspecLink> links = new ArrayList<>();
    private final List<BasicLease> leases = new ArrayList<>();
    private final List<BasicChannel> channels = new ArrayList<>();
    private final List<BasicGeantTestbedType> geantTestbedTypes = new ArrayList<>();
    private final List<BasicOpenflowDataPath> openflowDataPaths = new ArrayList<>();
    private final List<BasicAddressPool> addressPools = new ArrayList<>();
    private final List<BasicExecuteAnsiblePlaybook> ansibleCookbooks = new ArrayList<>();
    private final List<BasicDistributeSshKeypair> distributeSshKeypairs = new ArrayList<>();
    private String origDefaultNamespace = null;
    private String origSchemaLocation = null;
    private final String rspecType;
    private final List<UserSpec> sshKeys = new ArrayList<>();
    private String expiresString;

    private Integer routableAddressesAvailable;
    private Integer routableAddressesConfigured;
    private Integer multiplexFactor;

    private final RspecFactory factory;

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

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

    @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) {
        assert node instanceof BasicRspecNode;
        nodes.add((BasicRspecNode) node);
    }

    @Override
    public void addLink(RspecLink link) {
        assert link instanceof BasicRspecLink;
        links.add((BasicRspecLink) link);
    }

    @Override
    public List<BasicRspecNode> getNodes() {
        return nodes;
    }

    @Override
    public List<BasicRspecLink> getLinks() {
        return links;
    }

    @Override
    public void addOpenflowDataPath(OpenflowDataPath dp) {
        assert dp instanceof BasicOpenflowDataPath;
        openflowDataPaths.add((BasicOpenflowDataPath) dp);
    }

    @Override
    public List<BasicOpenflowDataPath> getOpenflowDataPaths() {
        return openflowDataPaths;
    }


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

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

    @Override
    public void addChannel(Channel channel) {

        assert channel instanceof BasicChannel;
        channels.add((BasicChannel) channel);
    }

    @Override
    public List<BasicChannel> getChannels() {
        return channels;
    }

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

    @Override
    public List<? extends GeantTestbedType> getGeantTestbedTypes() {
        return geantTestbedTypes;
    }

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

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

    @Override
    public List<BasicAddressPool> getAddressPools() {
        return addressPools;
    }

    @Override
    public AddressPool getAddressPoolByClientId(String clientId) {
        for (BasicAddressPool addressPool : addressPools) {
            if (Objects.equals(clientId, addressPool.getClientId()))
                return addressPool;
        }
        return null;
    }

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

    @SuppressWarnings("SuspiciousMethodCalls")
    @Override
    public boolean deleteExecuteAnsiblePlaybook(ExecuteAnsiblePlaybook executeAnsiblePlaybook) {
        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));
    }

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

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

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

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

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

    @Override
    public BasicChannel getChannelByComponentId(GeniUrn urn) {
        for (BasicChannel channel : channels)
            if (Objects.equals(channel.getComponentId(), urn))
                return channel;
        return null;
    }


    @Override
    public List<BasicRspecNode> getNodesByAuthority(Server server) {
        return nodes.stream()
                .filter(node -> node.getComponentManagerId().equalsIgnoringSubAuth(server.getDefaultComponentManagerUrn()))
                .collect(Collectors.toList());
    }

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

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

    @Override
    public void deleteNode(RspecNode node) {
        assert node instanceof BasicRspecNode;
        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(RspecInterface::delete);
        nodes.remove(node);
        linksToDelete.forEach(this::deleteLink);
    }

    @Override
    public void deleteLink(RspecLink link) {
        assert link instanceof BasicRspecLink;
        List<RspecInterface> ifacesToDelete = new ArrayList<>(link.getInterfaces());
        ifacesToDelete.forEach(RspecInterface::delete);
        links.remove(link);
    }

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

    @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 (expiresString == null) return null;
        return RFC3339Util.rfc3339StringToDate(expiresString);
    }

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

    @Override
    public String getExpiresString() {
        return expiresString;
    }

    @Override
    public void setExpires(String expires) {
        this.expiresString = expires;
    }


    @Override
    public Integer getRoutableAddressesAvailable() {
        return routableAddressesAvailable;
    }

    @Override
    public Integer getRoutableAddressesConfigured() {
        return routableAddressesConfigured;
    }

    @Override
    public void setRoutableAddressesAvailable(Integer routableAddressesAvailable) {
        this.routableAddressesAvailable = routableAddressesAvailable;
    }

    @Override
    public void setRoutableAddressesConfigured(Integer routableAddressesConfigured) {
        this.routableAddressesConfigured = routableAddressesConfigured;

    }

    @Override
    public GeantTestbedType getGeantTestbedTypeByName(String geantTestbedTypeName) {
        for (BasicGeantTestbedType geantTestbedType : geantTestbedTypes) {
            if (Objects.equals(geantTestbedType.getName(), geantTestbedTypeName))
                return geantTestbedType;
        }
        return null;
    }

    @Override
    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);

        return new ArrayList<>(urns);
    }

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

    @Override
    public void setSshKeys(List<UserSpec> newKeys) {
        this.sshKeys.clear();
        this.sshKeys.addAll(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 BasicRspecInterface getInterfaceByClientId(String clientId) {
        for (BasicRspecNode node : nodes) {
            BasicRspecInterface iface = node.getInterfaceByClientId(clientId);

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

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

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

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

    @Override
    public void setMultiplexFactor(Integer multiplexFactor) {
        this.multiplexFactor = 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/...)*/);
    }
}
