/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2024 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jboss.installer.postinstall.task;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import org.jboss.installer.auto.AutomaticInstallationParsingException;
import org.jboss.installer.auto.InstallationDataSerializer;
import org.jboss.installer.auto.ListXMLEventReader;
import org.jboss.installer.core.InstallationData;

public class PortBindingConfig implements InstallationData.PostInstallConfig {
    public static final String OFFSET_TAG = "offset";
    public static final String PORT_CONFIG_TAG = "port-config";
    public static final String MANAGEMENT_INTERFACE_TAG = "management-interface";
    public static final String SOCKET_BINDINGS_TAG = "socket-bindings";
    public static final String SOCKET_BINDING_TAG = "socket-binding";
    public static final String KEY_ATTRIBUTE = "key";
    public static final String PROPERTY_ATTRIBUTE = "property";
    public static final String MCAST_ADDRESS_ATTRIBUTE = "mcastAddress";
    public static final String PORT_VALUE_ATTRIBUTE = "portValue";
    public static final String MCAST_PORT_ATTRIBUTE = "mcastPort";
    public static final String CONFIG_ATTRIBUTE = "config";
    private int offset;
    private Map<String, List<SocketBinding>> socketBindings = new HashMap<>();
    private SocketBinding managementInterface = null;

    public PortBindingConfig() {
        // no-op for deserializer
    }

    public PortBindingConfig(int offset) {
        this.offset = offset;
    }

    public int getOffset() {
        return offset;
    }

    public List<SocketBinding> getPorts(String config) {
        return socketBindings.get(config);
    }

    public void setPorts(String config, List<SocketBinding> bindings) {
        socketBindings.put(config, bindings);
    }

    public void removePorts(String config) {
        socketBindings.remove(config);
    }

    public void setManagementInterfacePort(SocketBinding socketBinding) {
        this.managementInterface = socketBinding;
    }

    public void removeManagementInterfacePort() {
        this.managementInterface = null;
    }

    public SocketBinding getManagementInterface() {
        return managementInterface;
    }

    @Override
    public XMLEventReader serialize(XMLEventFactory eventFactory, Set<String> variables) {
        final ArrayList<XMLEvent> events = new ArrayList<>();
        events.add(eventFactory.createStartElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, PORT_CONFIG_TAG));
        events.add(eventFactory.createAttribute(OFFSET_TAG, "" + offset));

        if (managementInterface != null) {
            events.add(eventFactory.createStartElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, MANAGEMENT_INTERFACE_TAG));
            events.addAll(serializeSocketBinding(eventFactory, managementInterface));
            events.add(eventFactory.createEndElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, MANAGEMENT_INTERFACE_TAG));
        }

        for (String key : socketBindings.keySet()) {
            List<SocketBinding> bindings = socketBindings.get(key);
            events.add(eventFactory.createStartElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, SOCKET_BINDINGS_TAG));
            events.add(eventFactory.createAttribute(CONFIG_ATTRIBUTE, key));
            for (SocketBinding binding : bindings) {
                events.addAll(serializeSocketBinding(eventFactory, binding));
            }
            events.add(eventFactory.createEndElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, SOCKET_BINDINGS_TAG));
        }

        events.add(eventFactory.createEndElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, PORT_CONFIG_TAG));

        return new ListXMLEventReader(events);
    }

    private List<XMLEvent> serializeSocketBinding(XMLEventFactory eventFactory, SocketBinding socketBindining) {
        final List<XMLEvent> events = new ArrayList<>();
        events.add(eventFactory.createStartElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, SOCKET_BINDING_TAG));
        if (socketBindining.getKey() != null) {
            events.add(eventFactory.createAttribute(KEY_ATTRIBUTE, socketBindining.getKey()));
        }
        if (socketBindining.getProperty() != null) {
            events.add(eventFactory.createAttribute(PROPERTY_ATTRIBUTE, socketBindining.getProperty()));
        }
        if (socketBindining.getMcastAddress() != null) {
            events.add(eventFactory.createAttribute(MCAST_ADDRESS_ATTRIBUTE, socketBindining.getMcastAddress()));
        }
        if (socketBindining.getPortValue() != null) {
            events.add(eventFactory.createAttribute(PORT_VALUE_ATTRIBUTE, socketBindining.getPortValue().toString()));
        }
        events.add(eventFactory.createAttribute(MCAST_PORT_ATTRIBUTE, "" + socketBindining.getMcastPort()));
        events.add(eventFactory.createEndElement(InstallationDataSerializer.PREFIX, InstallationDataSerializer.NS, SOCKET_BINDING_TAG));
        return events;
    }

    @Override
    public void deserialize(XMLEventReader reader, BiFunction<String, String, String> variableResolver) throws AutomaticInstallationParsingException {
        try {
            while (reader.hasNext()) {
                final XMLEvent xmlEvent = reader.nextEvent();
                if (xmlEvent.isStartElement()) {
                    final StartElement elem = xmlEvent.asStartElement();
                    if (elem.getName().getLocalPart().equals(PORT_CONFIG_TAG)) {
                        this.offset = Integer.parseInt(elem.getAttributeByName(new QName(OFFSET_TAG)).getValue());
                    } else if (elem.getName().getLocalPart().equals(MANAGEMENT_INTERFACE_TAG)) {
                        while (reader.hasNext()) {
                            final XMLEvent xmlEvent1 = reader.nextEvent();
                            if (xmlEvent1.isEndElement() && xmlEvent1.asEndElement().getName().getLocalPart().equals(MANAGEMENT_INTERFACE_TAG)) {
                                break;
                            }
                            if (xmlEvent1.isStartElement()) {
                                if (xmlEvent1.asStartElement().getName().getLocalPart().equals(SOCKET_BINDING_TAG)) {
                                    StartElement bindingElement = xmlEvent1.asStartElement();
                                    this.managementInterface = deserializeSocketBinding(bindingElement);
                                } else {
                                    throw InstallationDataSerializer.unexpectedElement(xmlEvent1.asStartElement());
                                }
                            }
                        }
                    } else if (elem.getName().getLocalPart().equals(SOCKET_BINDINGS_TAG)) {
                        final String config = elem.getAttributeByName(new QName(CONFIG_ATTRIBUTE)).getValue();
                        List<SocketBinding> bindings = new ArrayList<>();
                        while (reader.hasNext()) {
                            final XMLEvent xmlEvent1 = reader.nextEvent();
                            if (xmlEvent1.isEndElement() && xmlEvent1.asEndElement().getName().getLocalPart().equals(SOCKET_BINDINGS_TAG)) {
                                break;
                            }
                            if (xmlEvent1.isStartElement()) {
                                if (xmlEvent1.asStartElement().getName().getLocalPart().equals(SOCKET_BINDING_TAG)) {
                                    bindings.add(deserializeSocketBinding(xmlEvent1.asStartElement()));
                                } else {
                                    throw InstallationDataSerializer.unexpectedElement(xmlEvent1.asStartElement());
                                }
                            }
                        }
                        socketBindings.put(config, bindings);
                    }
                }
            }
        } catch (XMLStreamException e) {
            throw InstallationDataSerializer.unableToParse(e);
        }
    }

    private SocketBinding deserializeSocketBinding(StartElement bindingElement) {
        final String key = readBindingAttribute(bindingElement, KEY_ATTRIBUTE);
        final String property = readBindingAttribute(bindingElement, PROPERTY_ATTRIBUTE);
        final String mcastAddress = readBindingAttribute(bindingElement, MCAST_ADDRESS_ATTRIBUTE);
        final String portValueText = readBindingAttribute(bindingElement, PORT_VALUE_ATTRIBUTE);
        Integer portValue = portValueText != null ? Integer.parseInt(portValueText) : null;
        final int mcastPort = Integer.parseInt(readBindingAttribute(bindingElement, MCAST_PORT_ATTRIBUTE));

        return new SocketBinding(key, property, mcastAddress, mcastPort, portValue);
    }

    private String readBindingAttribute(StartElement bindingElement, String key) {
        Attribute attr = bindingElement.getAttributeByName(new QName(key));
        return (attr != null) ? attr.getValue() : null;
    }

    public static class SocketBinding {
        private final String key;
        private final String property;
        private final String mcastAddress;
        private final Integer portValue;
        private final int mcastPort;

        public SocketBinding(String key, String portProperty, int portValue) {
            this.key = key;
            this.property = portProperty;
            this.portValue = portValue;
            this.mcastAddress = null;
            this.mcastPort = 0;
        }

        public SocketBinding(String key, String property, String mcastAddress, int mcastPort, Integer port) {
            this.key = key;
            this.property = property;
            this.portValue = port;
            this.mcastAddress = mcastAddress;
            this.mcastPort = mcastPort;
        }

        public String getKey() {
            return key;
        }

        public String getProperty() {
            return property;
        }

        public String getMcastAddress() {
            return mcastAddress;
        }

        public Integer getPortValue() {
            return portValue;
        }

        public int getMcastPort() {
            return mcastPort;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            SocketBinding that = (SocketBinding) o;
            return mcastPort == that.mcastPort && Objects.equals(key, that.key) && Objects.equals(property, that.property) && Objects.equals(mcastAddress, that.mcastAddress) && Objects.equals(portValue, that.portValue);
        }

        @Override
        public int hashCode() {
            return Objects.hash(key, property, mcastAddress, portValue, mcastPort);
        }
    }
}
