/*
 * RHQ Management Platform
 * Copyright (C) 2005-2009 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package org.rhq.plugins.apache.util;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.plugins.apache.parser.ApacheDirective;
import org.rhq.plugins.apache.parser.ApacheDirectiveTree;

/**
 * Utility class to extract various HTTP addresses from Augeas loaded Apache configuration.
 * 
 * @author Lukas Krejci
 */
public enum HttpdAddressUtility {

    APACHE_1_3 {
        
        public List<Address> getAllMainServerAddresses(ApacheDirectiveTree ag, boolean substituteWildcards) {
            try {
                List<ApacheDirective> ports = ag.search("/Port");
                List<ApacheDirective> bindAddresses = ag.search("/BindAddress");
                List<ApacheDirective> listens = ag.search("/Listen");

                String port = "80"; //this is the default in apache 1.3
                String bindAddress = null;
                
                List<Address> addresses = new ArrayList<Address>();
                
                if (ports.size() > 0) {
                    List<String>values = ports.get(0).getValues();
                    if (values.size()>0)                           
                       port =  values.get(0);
                }
                
                if (bindAddresses.size() > 0) {
                    List<String>values = bindAddresses.get(0).getValues();
                    if (values.size()>0)                        
                    bindAddress =  values.get(0);
                }
                
                //listen directives take precedence over port/bindaddress combo
                if (listens.size() > 0) {
                    for(ApacheDirective l : listens) {
                        addresses.add(parseListen(l.getValues().get(0)));
                    }
                } else {
                    addresses.add(new Address(bindAddress, Integer.parseInt(port)));
                }
                
                for (Address address : addresses) {
                    
                    if (!address.isPortDefined()) {
                        address.port = 80;
                    }
                    
                    if (substituteWildcards) {
                        substituteWildcards(ag, address);
                    }
                }
                
                return addresses;
            } catch (Exception e) {
                log.warn("Failed to obtain main server address.", e);

                return null;
            }
        }
    },
    APACHE_2_x {
        
        public List<Address> getAllMainServerAddresses(ApacheDirectiveTree ag, boolean substituteWildcards) {
            try {
                List<Address> ret = new ArrayList<Address>();
                
                for(ApacheDirective n : ag.search("/Listen")) {
                    Address addr = parseListen(n.getValues().get(0));
                    
                    if (substituteWildcards) {
                        substituteWildcards(ag, addr);
                    }
                    
                    ret.add(addr);
                }
                
                return ret;
            } catch (Exception e) {
                log.warn("Failed to obtain main server address.", e);

                return null;
            }
        }
    };

    private static final Log log = LogFactory.getLog(HttpdAddressUtility.class);

    public static final String BOGUS_HOST_WITHOUT_FORWARD_DNS = "bogus_host_without_forward_dns";
    public static final String BOGUS_HOST_WITHOUT_REVERSE_DNS = "bogus_host_without_reverse_dns";
    
    public static HttpdAddressUtility get(String version) {
        return version.startsWith("1.") ? APACHE_1_3 : APACHE_2_x;
    }
    
    public static class Address {
        public String host;
        public int port = -1;
        public String scheme = "http";
        
        public static final String WILDCARD = "*";
        public static final String DEFAULT_HOST = "_default_";
        public static final int PORT_WILDCARD_VALUE = 0;
        public static final int NO_PORT_SPECIFIED_VALUE = -1;
        
        public Address(String host, int port) {
            this.host = host;
            this.port = port;
        }

        public Address(String scheme, String host, int port) {
            this(host, port);
            this.scheme = scheme;
        }
        /**
         * A simple parser of the provided address into host and port
         * sections.
         * <p>
         * This is equivalent to calling {@link #parse(String, String)} with
         * the default scheme "http".
         * 
         * @param address the address to parse
         * @return an instance of Address with host and port set accordingly
         */
        public static Address parse(String address) {
            return parse(address, "http");
        }

        /**
         * Parses given address into an Address object and assigns a default scheme if none
         * is present in the address itself.
         * 
         * @param address the address to parse
         * @param defaultScheme the default scheme to apply or null if no scheme is required by default
         * @return the parsed address
         */
        public static Address parse(String address, String defaultScheme) {
            String scheme = defaultScheme;
            int schemeSpecIdx = address.indexOf("://");
            if (schemeSpecIdx >= 0) {
                scheme = address.substring(0, schemeSpecIdx);
                address = address.substring(schemeSpecIdx + "://".length());
            }
            
            int lastColonIdx = address.lastIndexOf(':');
            if (lastColonIdx == -1) {
                return new Address(scheme, address, NO_PORT_SPECIFIED_VALUE);
            } else {
                int lastRightBracketPos = address.lastIndexOf(']');
                if (lastColonIdx > lastRightBracketPos) {
                    String host = address.substring(0, lastColonIdx);
                    String portSpec = address.substring(lastColonIdx + 1);
                    
                    int port = NO_PORT_SPECIFIED_VALUE;
                    if (WILDCARD.equals(portSpec)) {
                        port = PORT_WILDCARD_VALUE;
                    } else {
                        port = Integer.parseInt(portSpec);
                    }
                    
                    return new Address(scheme, host, port);
                } else {
                    //this is an IP6 address without a port spec
                    return new Address(scheme, address, NO_PORT_SPECIFIED_VALUE);
                }
            }
        }
        
        public boolean isPortWildcard() {
            return port == PORT_WILDCARD_VALUE;
        }
        
        public boolean isPortDefined() {
            return port != NO_PORT_SPECIFIED_VALUE;
        }
        
        public boolean isHostWildcard() {
            return WILDCARD.equals(host);
        }
        
        public boolean isHostDefault() {
            return DEFAULT_HOST.equals(host);
        }
        
        @Override
        public int hashCode() {
            int hash = port;
            if (host != null)
                hash *= host.hashCode();
            return hash;
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof Address))
                return false;

            Address o = (Address) other;

            return safeEquals(host, o.host) && this.port == o.port;
        }
        
        /**
         * This differs from equals in the way that it considers wildcard values:
         * <ul>
         * <li>wildcard host matches any host
         * <li>default host matches default host
         * <li>wildcard port matches any port
         * <li>undefined port matches undefined port
         * </ul>
         * The addresses match if both address and port match.
         * 
         * @param other the address to match
         * @param whether to match the scheme as well
         * @return true if the addresses match according to the rules described above, false otherwise
         */
        public boolean matches(Address other, boolean matchSchemes) {
            if (matchSchemes && !safeEquals(scheme, other.scheme)) {
                return false;
            }
            
            if (!WILDCARD.equals(host) && !WILDCARD.equals(other.host) && !safeEquals(host, other.host)) {
                return false;
            }
            
            if (PORT_WILDCARD_VALUE != port && PORT_WILDCARD_VALUE != other.port && port != other.port) {
                return false;
            }
            
            return true;
        }
        
        @Override
        public String toString() {
            return toString(true, true);
        }
        
        public String toString(boolean includeScheme, boolean interpretWildcardPort) {
            StringBuilder bld = new StringBuilder();
            
            if (includeScheme && scheme != null) {
                bld.append(scheme).append("://");
            }

            if (host != null) {
                bld.append(host);
                
                if (port != NO_PORT_SPECIFIED_VALUE) {
                    bld.append(":");
                }
            }

            if (port != NO_PORT_SPECIFIED_VALUE) {
                if (port == PORT_WILDCARD_VALUE && interpretWildcardPort) {
                    bld.append(WILDCARD);
                } else {
                    bld.append(port);
                }
            }
            
            return bld.toString();
        }
        
        private static boolean safeEquals(Object a, Object b) {
            return a == null ? b == null : a.equals(b);
        }
    }

    /**
     * This returns all the addresses the server listens on.
     * 
     * @param ag the tree of the httpd configuration
     * @param substituteWildcards true if wildcard substitution should be made on host and port specs
     * @return the addresses or null on failure
     */
    public abstract List<Address> getAllMainServerAddresses(ApacheDirectiveTree ag, boolean substituteWildcards);
    
    /**
     * This just constructs a first available address under which the server or one of its virtual hosts can be reached.
     * 
     * @param ag the tree of the httpd configuration
     * @param limitToHost if non-null and different from {@link Address#DEFAULT_HOST} and {@link Address#WILDCARD}, 
     * the sample address is looked for only for the given host
     * @param limitToPort if > 0, the sample address is looked for only for the given port
     * @return the address or null on failure
     */
    public Address getMainServerSampleAddress(ApacheDirectiveTree ag, String limitToHost, int limitToPort) {
        List<Address> addressesToMatch = getAllMainServerAddresses(ag, false);
        
        if (addressesToMatch == null) {
            return null;
        }
        
        for (Address address : addressesToMatch) {
            if (isAddressConforming(address, limitToHost, limitToPort, false)) {
                substituteWildcards(ag, address);
                if (address.scheme == null) {
                    address.scheme = "http";
                }
                return address;
            }
        }
        
        return null;
    }

    /**
     * This constructs an address on which given virtual host can be accessed.
     * 
     * @param ag the augeas tree of the httpd configuration
     * @param virtualHost the port or address:port of the virtual host
     * @param serverName the server name for the namebased virtual hosts (or null if the virtual host is ip based)
     * @param legacyWildcardHostHandling use the legacy handling of wildcard hosts. This should always be false unless you are calling this method
     * from the code generating the legacy resource keys during vhost upgrade
     * @return the address on which the virtual host can be accessed or null on error
     */
    public Address getVirtualHostSampleAddress(ApacheDirectiveTree ag, String virtualHost, String serverName, boolean legacyWildcardHostHandling) {
        try {
            Address addr = Address.parse(virtualHost);
            if (addr.isHostDefault() || addr.isHostWildcard()) {
                Address serverAddr = null;
                if (legacyWildcardHostHandling) {
                    serverAddr = getLocalhost(addr.port);
                } else {
                    serverAddr = getMainServerSampleAddress(ag, null, addr.port);
                }
                if (serverAddr == null)
                    return null;
                addr.host = serverAddr.host;
            }
            
            if (serverName != null) {
                updateWithServerName(addr, serverName);
            }

            return addr;
        } catch (Exception e) {
            log.warn("Failed to obtain virtual host address.", e);
            return null;
        }
    }
    
    public Address getHttpdInternalMainServerAddressRepresentation(ApacheDirectiveTree runtimeConfig) {
        Address ret = null;
        
        List<ApacheDirective> serverNames = runtimeConfig.search("/ServerName");
        if (serverNames.size() == 0) {
            //no servername directive in the apache config
            ret = new Address(Address.WILDCARD, Address.NO_PORT_SPECIFIED_VALUE);
            try {
                ret.host = InetAddress.getLocalHost().getCanonicalHostName();
            } catch (UnknownHostException e) {
                ret.host = "127.0.0.1";
            }
            
            ret.port = 0;
        } else {
            String serverName = serverNames.get(serverNames.size() - 1).getValuesAsString();
            ret = HttpdAddressUtility.Address.parse(serverName);
            if (!ret.isPortDefined()) {
                ret.port = 0;
            }
        }
        
        return ret;
    }
    
    public Address getHttpdInternalVirtualHostAddressRepresentation(ApacheDirectiveTree runtimeConfig, String virtualHost, String serverName) {
        Address ret = null;
        
        if (serverName != null) {
            ret = Address.parse(serverName);
            if (!ret.isPortDefined()) {
                ret.port = 0;
            }
            
            //servername is taken literally and no reverse dns lookup is made
            //if the servername host is an IP address. We're done here...
        } else {
            ret = Address.parse(virtualHost);
            if (!ret.isPortDefined() || ret.isPortWildcard() || ret.isHostDefault() || ret.isHostWildcard()) {
                Address mainAddress = getHttpdInternalMainServerAddressRepresentation(runtimeConfig);
                
                if (!ret.isPortDefined() || ret.isPortWildcard()) {
                    ret.port = mainAddress.port;
                }
                
                if (ret.isHostDefault() || ret.isHostWildcard()) {
                    ret.host = mainAddress.host;
                }
            }
            
            //if the vhost hostname is an IP address, a reverse dns lookup is attempted
            //to get the actual hostname.
            //the BOGUS* constants are what the apache actually uses to identify such
            //"error" conditions.
            try {
                InetAddress iAddr = InetAddress.getByName(ret.host);
                String reverseLookup = iAddr.getHostName();
                if (iAddr.getHostAddress().equals(reverseLookup)) {
                    ret.host = BOGUS_HOST_WITHOUT_REVERSE_DNS;
                } else {
                    ret.host = reverseLookup;
                }
            } catch (UnknownHostException e) {
                ret.host = BOGUS_HOST_WITHOUT_FORWARD_DNS;

                //weird, as it seems, apache uses the port of the main server
                //with the unknown host even if the port was specified in the vhost
                //definition
                Address mainAddress = getHttpdInternalMainServerAddressRepresentation(runtimeConfig);
                ret.port = mainAddress.port;
            }
        }
        
        return ret;
    }
    
    public static Address parseListen(String listenValue) {
        Address ret = Address.parse(listenValue, null);
        if (!ret.isPortDefined()) {
            try {
                ret.port = Integer.parseInt(ret.host);
            } catch (NumberFormatException e) {
                return null;
            }
            ret.host = null;
        }
        
        return ret;
    }
    
    private static void substituteWildcards(ApacheDirectiveTree ag, Address address) {
        if (address.isPortWildcard()) {
            address.port = 80;
        }
        
        if (address.host == null || address.isHostDefault() || address.isHostWildcard()) {
            Address localhost = getLocalhost(address.port);
            address.host = localhost.host;
        }
        
        updateWithServerName(address, ag);
        
    }
    
    /**
     * Checks that given address represents a possibly wildcarded limitingHost and limitingPort values.
     * 
     * @param listen the address to check
     * @param limitingHost the host to limit to. The null value or the {@link Address#DEFAULT_HOST} 
     * or the {@link Address#WILDCARD} are not considered limiting
     * @param limitingPort the port to limit the address to. Values &lt;= 0 are not considered limiting
     * @param snmpModuleCompatibleMode the snmp module represents both port 80 and port wildcard (*) as '0'. 
     * If this flag is set to true, this method takes that into account.
     * @return
     */
    public static boolean isAddressConforming(Address listen, String limitingHost, int limitingPort, boolean snmpModuleCompatibleMode) {
        if (Address.DEFAULT_HOST.equals(limitingHost) || Address.WILDCARD.equals(limitingHost)) {
            limitingHost = null;
        }
        
        boolean hostOk = limitingHost == null;
        boolean portOk = limitingPort <= 0;
        
        //listen.host == null means that server listens on all addresses 
        if (!hostOk && (listen.host == null || limitingHost.equals(listen.host))) {
            hostOk = true;
        }
        
        int listenPort = listen.port;
        
        //this stupid 80 = 0 rule is to conform with snmp module
        //the problem is that snmp module represents both 80 and * port defs as 0, 
        //so whatever we do, we might mismatch the vhost. But there's no working around that
        //but to modify the snmp module itself.
        if (snmpModuleCompatibleMode) {
            if (limitingPort == 80) {
                limitingPort = 0;
            }
            if (listenPort == 80) {
                listenPort = 0;
            }
        }
        
        if (!portOk && limitingPort == listenPort) {
            portOk = true;
        }
        
        return hostOk && portOk;
    }
    
    private static Address getLocalhost(int port) {
        try {
            return new Address(InetAddress.getLocalHost().getHostAddress(), port);
        } catch (UnknownHostException e) {
            //well, this is bad, we can't get address of the localhost. let's use the force...
            return new Address("127.0.0.1", port);
        }
    }
    
    private static void updateWithServerName(Address address, ApacheDirectiveTree config) {
        //check if there is a ServerName directive
        List<ApacheDirective> serverNameNodes = config.search("/ServerName");

        //if there is a ServerName directive, check that the address
        //we're returning indeed corresponds to it. This might not
        //be the case if the server listens on more than one interfaces.
        if (serverNameNodes.size() > 0) {
            String serverName = serverNameNodes.get(0).getValuesAsString();
            updateWithServerName(address, serverName);
        }
    }
    
    private static void updateWithServerName(Address address, String serverName) {
        //the configuration may be invalid and/or the hostname can be unresolvable.
        //we try to match the address with the servername first by IP address
        //but if that fails (i.e. the hostname couldn't be resolved to an IP)
        //we try to simply match the hostnames themselves.
        
        Address serverAddr = Address.parse(serverName);
        String ipFromServerName = null;
        String ipFromAddress = null;
        String hostFromServerName = null;
        String hostFromAddress = null;
        boolean lookupFailed = false;

        try {
            InetAddress addrFromServerName = InetAddress.getByName(serverAddr.host);
            ipFromServerName = addrFromServerName.getHostAddress();
            hostFromServerName = addrFromServerName.getHostName();
        } catch (UnknownHostException e) {
            ipFromServerName = serverAddr.host;
            hostFromServerName = serverAddr.host;
            lookupFailed = true;
        }
        
        try {
            InetAddress addrFromAddress = InetAddress.getByName(address.host);
            ipFromAddress = addrFromAddress.getHostAddress();
            hostFromAddress = addrFromAddress.getHostName();
        } catch (UnknownHostException e) {
            ipFromAddress = address.host;
            hostFromAddress = address.host;
            lookupFailed = true;
        }
        
        if (ipFromAddress.equals(ipFromServerName) || (lookupFailed && (hostFromAddress.equals(hostFromServerName)))) {
            address.scheme = serverAddr.scheme;
            address.host = serverAddr.host;
        }
    }
}
