/**
 * Copyright (c) 2009--2015 Red Hat, Inc.
 *
 * This software is licensed to you under the GNU General Public License,
 * version 2 (GPLv2). There is NO WARRANTY for this software, express or
 * implied, including the implied warranties of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
 * along with this software; if not, see
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
 *
 * Red Hat trademarks are not licensed under GPLv2. No permission is
 * granted to use or replicate Red Hat trademarks that are incorporated
 * in this software or its documentation.
 */
package com.redhat.rhn.domain.server;

import com.redhat.rhn.common.conf.Config;
import com.redhat.rhn.common.conf.ConfigDefaults;
import com.redhat.rhn.domain.BaseDomainHelper;
import com.redhat.rhn.domain.Identifiable;
import com.redhat.rhn.domain.channel.Channel;
import com.redhat.rhn.domain.common.ProvisionState;
import com.redhat.rhn.domain.config.ConfigChannel;
import com.redhat.rhn.domain.config.ConfigChannelListProcessor;
import com.redhat.rhn.domain.config.ConfigChannelType;
import com.redhat.rhn.domain.config.ConfigurationFactory;
import com.redhat.rhn.domain.entitlement.Entitlement;
import com.redhat.rhn.domain.entitlement.VirtualizationEntitlement;
import com.redhat.rhn.domain.org.CustomDataKey;
import com.redhat.rhn.domain.org.Org;
import com.redhat.rhn.domain.org.OrgFactory;
import com.redhat.rhn.domain.user.User;
import com.redhat.rhn.manager.configuration.ConfigurationManager;
import com.redhat.rhn.manager.entitlement.EntitlementManager;
import com.redhat.rhn.manager.kickstart.cobbler.CobblerXMLRPCHelper;
import com.redhat.rhn.manager.system.SystemManager;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.log4j.Logger;
import org.cobbler.CobblerConnection;
import org.cobbler.SystemRecord;

import java.net.IDN;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
 * Server - Class representation of the table rhnServer.
 *
 * @version $Rev$
 */
public class Server extends BaseDomainHelper implements Identifiable {

    /**
     * Logger for this class
     */
    private static Logger log = Logger.getLogger(Server.class);

    private Boolean ignoreEntitlementsForMigration;

    private Long id;
    private Org org;
    private String digitalServerId;
    private String os;
    private String release;
    private String name;
    private String description;
    private String info;
    private String secret;
    private User creator;
    private String autoUpdate;
    private String runningKernel;
    private Long lastBoot;
    private ServerArch serverArch;
    private ProvisionState provisionState;
    private Date channelsChanged;
    private Date created;
    private String cobblerId;
    private Set<Device> devices;
    private ServerInfo serverInfo;
    private CPU cpu;
    private ServerLock lock;
    private ServerUuid serverUuid;
    private Set<Note> notes;
    private Set<Network> networks;
    private Ram ram;
    private Dmi dmi;
    private NetworkInterface primaryInterface;
    private Set<NetworkInterface> networkInterfaces;
    private Set<CustomDataValue> customDataValues;
    private Set<Channel> channels;
    private List<ConfigChannel> configChannels = new ArrayList<ConfigChannel>();
    private Set<ConfigChannel> localChannels = new HashSet<ConfigChannel>();
    private Location serverLocation;
    private Set<VirtualInstance> guests = new HashSet<VirtualInstance>();
    private VirtualInstance virtualInstance;
    private PushClient pushClient;
    private final ConfigChannelListProcessor configListProc =
        new ConfigChannelListProcessor();
    private Set<ServerHistoryEvent> history;
    private Set<InstalledPackage> packages;
    private ProxyInfo proxyInfo;
    private Set<? extends ServerGroup> groups;
    private Set<Capability> capabilities;
    private CrashCount crashCount;
    private Set<Crash> crashes;

    public static final String VALID_CNAMES = "valid_cnames_";

    /**
     * @return Returns the capabilities.
     */
    public Set<Capability> getCapabilities() {
        return capabilities;
    }



    /**
     * @param capabilitiesIn The capabilities to set.
     */
    public void setCapabilities(Set<Capability> capabilitiesIn) {
        capabilities = capabilitiesIn;
    }


    /**
     * @return Returns the groups.
     */
    protected Set<? extends ServerGroup> getGroups() {
        return groups;
    }


    /**
     * @param groupsIn The groups to set.
     */
    protected void setGroups(Set<? extends ServerGroup> groupsIn) {
        groups = groupsIn;
    }

    /**
     * @return the proxyInfo
     */
    public ProxyInfo getProxyInfo() {
        return proxyInfo;
    }

    /**
     * the proxy information to set
     * @param proxy the proxyInfo to set
     */
    public void setProxyInfo(ProxyInfo proxy) {
        this.proxyInfo = proxy;
    }

    /**
     * Retrieves the local override channel associated with this system.
     * @return the Local Override Channel or create one if none exists
     */
    public ConfigChannel getLocalOverride() {
        return  findLocal(ConfigChannelType.local());
    }

    /**
     * Retrieves the local override channel associated with this system.
     * @return the Local Override Channel or NULL if there's none created yet
     */
    public ConfigChannel getLocalOverrideNoCreate() {
        ensureConfigManageable();
        ConfigChannel channel = null;
        for (Iterator<ConfigChannel> itr = localChannels.iterator(); itr
                .hasNext();) {
            ConfigChannel ch = itr.next();
            if (ch.getConfigChannelType().equals(ConfigChannelType.local())) {
                channel = ch;
                break;
            }
        }
        return channel;
    }

    /**
     *
     * @param ch Override channel to set
     */
    public void setLocalOverride(ConfigChannel ch) {
        setLocalType(ch, ConfigChannelType.local());
    }

    private void setLocalType(ConfigChannel channel,
            ConfigChannelType cct) {

        ConfigChannel ch =  findLocal(cct);
        if (ch != null) {
            localChannels.remove(ch);
        }
        localChannels.add(channel);
    }

    protected void setLocalChannels(Set<ConfigChannel> chls) {
        localChannels = chls;
    }

    protected Set<ConfigChannel> getLocalChannels() {
        return localChannels;
    }

    /**
     * Used for retrieving Local/Sandbox override channels since the process is
     *  exacly the same. Creates the channel if it does not exist.
     * @param cct Config Channel type .. (local or sandbox)
     * @return Config channel associated with the given type
     */
    private ConfigChannel findLocal(ConfigChannelType cct) {

        assert localChannels.size() <= 2 : "More than two local override  channels" +
                "Associated with this server." +
                "There should be NO more than Two" +
                " Override Channels associated";
        ensureConfigManageable();
        for (Iterator<ConfigChannel> itr = localChannels.iterator(); itr
                .hasNext();) {
            ConfigChannel ch = itr.next();
            ConfigChannelType  item = ch.getConfigChannelType();
            if (cct.equals(item)) {
                return ch;
            }
        }

        //We automatically create local config channels, so
        //if we didn't find one, we just haven't created it yet.
        ConfigChannel channel = ConfigurationFactory.createNewLocalChannel(this, cct);

        //TODO: Adding the new channel to the set of local channels should
        //happen in the createNewLocalChannel method.  However, the way things
        //are currently set up, I have to work with the member variable, because using
        //accessors and mutators would create an infinite loop.  Fix this setup.
        localChannels.add(channel);
        setLocalChannels(localChannels);
        return channel;
    }

    /**
     * Retrieves the sandbox override channel associated with this system.
     * @return the Sandbox Override Channel or create one if none exists
     */
    public ConfigChannel getSandboxOverride() {
        return findLocal(ConfigChannelType.sandbox());
    }

    /**
     * Retrieves the sandbox override channel associated with this system.
     * @return the Sandbox Override Channel or NULL if there's none created yet
     */
    public ConfigChannel getSandboxOverrideNoCreate() {
        ensureConfigManageable();
        ConfigChannel channel = null;
        for (Iterator<ConfigChannel> itr = localChannels.iterator(); itr
                .hasNext();) {
            ConfigChannel ch = itr.next();
            if (ch.getConfigChannelType().equals(ConfigChannelType.sandbox())) {
                channel = ch;
                break;
            }
        }
        return channel;
    }

    /**
     *
     * @param ch sets the sandbox override channel
     */
    public void setSandboxOverride(ConfigChannel ch) {
        setLocalType(ch, ConfigChannelType.sandbox());
    }

    /**
     * ONLY TO BE USED FOR/BY HIBERNATE
     * @param configChannelsIn The configChannels to set.
     */
    protected void setConfigChannelsHibernate(
            List<ConfigChannel> configChannelsIn) {
        configChannels = configChannelsIn;
        for (Iterator<ConfigChannel> itr = configChannels.iterator(); itr
                .hasNext();) {
            if (itr.next() == null) {
                itr.remove();
            }
        }
    }

    /**
     * ONLY TO BE USED FOR/BY HIBERNATE
     *
     * @return List of config channels
     */
    protected List<ConfigChannel> getConfigChannelsHibernate() {
        return configChannels;
    }

    /**
     * @return Returns the ServerConfigChannels mappings currently available
     * to the server based on it's entitlements.
     */
    public List <ConfigChannel> getConfigChannels() {
        ensureConfigManageable();
        return getConfigChannelsHibernate();
    }

    /**
     * @return Returns the number of configuration channels associated with
     * the server.
     */
    public int getConfigChannelCount() {
        return getConfigChannelsHibernate().size();
    }

    private void ensureConfigManageable() {
        if (!getIgnoreEntitlementsForMigration()) {
            ConfigurationManager.getInstance().ensureConfigManageable(this);
        }
    }

    /**
     * subscribes a channel to a system, giving it the
     * highest value for the  position (or the lowest priority)
     * @param cc The config channel to subscribe to
     */
    public void subscribe(ConfigChannel cc) {
        configListProc.add(getConfigChannels(), cc);
    }
    /**
     * subscribes a channel to a system at the given position
     * @param cc the channel to subscribe
     * @param position the positon/ranking of the channel in the system list,
     *                  must be {@literal > 0}
     */
    public void subscribeAt(ConfigChannel cc, int position) {
        configListProc.add(getConfigChannels(), cc, position);
    }

    /**
     * @param cc the ConfigChannel to unsubscribe
     * @return returns true if the remove operation succeeded
     */
    public boolean unsubscribe(ConfigChannel cc) {
        return configListProc.remove(getConfigChannels(), cc);
    }

    /**
     * Protected constructor
     */
    protected Server() {
        devices = new HashSet<Device>();
        notes = new HashSet<Note>();
        networks = new HashSet<Network>();
        networkInterfaces = new HashSet<NetworkInterface>();
        customDataValues = new HashSet<CustomDataValue>();

        ignoreEntitlementsForMigration = Boolean.FALSE;
    }

    /**
     * @return Returns the serverInfo.
     */
    public ServerInfo getServerInfo() {
        return serverInfo;
    }
    /**
     * @param serverInfoIn The serverInfo to set.
     */
    public void setServerInfo(ServerInfo serverInfoIn) {
        this.serverInfo = serverInfoIn;
    }
    /**
     * Gets the last checkin date for this server
     * @return last checkin date
     */
    public Date getLastCheckin() {
        return serverInfo.getCheckin();
    }
    /**
     * Gets the number of times this server has checked in
     * @return number of times this server has checked in.
     */
    public Long getCheckinCount() {
        return serverInfo.getCheckinCounter();
    }
    /**
     * Getter for id
     *
     * @return Long to get
     */
    public Long getId() {
        return this.id;
    }

    /**
     * Setter for id
     *
     * @param idIn to set
     */
    public void setId(Long idIn) {
        this.id = idIn;
    }

    /**
     * @return Returns the org.
     */
    public Org getOrg() {
        return org;
    }

    /**
     * @param o The org to set.
     */
    public void setOrg(Org o) {
        this.org = o;
    }

    /**
     * Getter for digitalServerId
     *
     * @return String to get
     */
    public String getDigitalServerId() {
        return this.digitalServerId;
    }

    /**
     * Setter for digitalServerId
     *
     * @param digitalServerIdIn to set
     */
    public void setDigitalServerId(String digitalServerIdIn) {
        this.digitalServerId = digitalServerIdIn;
    }

    /**
     * Getter for os
     *
     * @return String to get
     */
    public String getOs() {
        return this.os;
    }

    /**
     * Setter for os
     *
     * @param osIn to set
     */
    public void setOs(String osIn) {
        this.os = osIn;
    }

    /**
     * Getter for release
     *
     * @return String to get
     */
    public String getRelease() {
        return this.release;
    }

    /**
     * Setter for release
     *
     * @param releaseIn to set
     */
    public void setRelease(String releaseIn) {
        this.release = releaseIn;
    }

    /**
     * Getter for name
     *
     * @return String to get
     */
    public String getName() {
        return this.name;
    }

    /**
     * Setter for name
     *
     * @param nameIn to set
     */
    public void setName(String nameIn) {
        this.name = nameIn;
    }

    /**
     * Getter for description
     *
     * @return String to get
     */
    public String getDescription() {
        return this.description;
    }

    /**
     * Setter for description
     *
     * @param descriptionIn to set
     */
    public void setDescription(String descriptionIn) {
        this.description = descriptionIn;
    }

    /**
     * Getter for info
     *
     * @return String to get
     */
    public String getInfo() {
        return this.info;
    }

    /**
     * Setter for info
     *
     * @param infoIn to set
     */
    public void setInfo(String infoIn) {
        this.info = infoIn;
    }

    /**
     * Getter for secret
     *
     * @return String to get
     */
    public String getSecret() {
        return this.secret;
    }

    /**
     * Setter for secret
     *
     * @param secretIn to set
     */
    public void setSecret(String secretIn) {
        this.secret = secretIn;
    }

    /**
     * @return Returns the creator.
     */
    public User getCreator() {
        return creator;
    }

    /**
     * @param c The creator to set.
     */
    public void setCreator(User c) {
        this.creator = c;
    }

    /**
     * Getter for autoUpdate
     *
     * @return String to get
     */
    public String getAutoUpdate() {
        return this.autoUpdate;
    }

    /**
     * Setter for autoUpdate
     *
     * @param autoUpdateIn to set
     */
    public void setAutoUpdate(String autoUpdateIn) {
        this.autoUpdate = autoUpdateIn;
    }

    /**
     * Getter for runningKernel
     *
     * @return String to get
     */
    public String getRunningKernel() {
        return this.runningKernel;
    }

    /**
     * Setter for runningKernel
     *
     * @param runningKernelIn to set
     */
    public void setRunningKernel(String runningKernelIn) {
        this.runningKernel = runningKernelIn;
    }

    /**
     * Getter for lastBoot
     *
     * @return Long to get
     */
    public Long getLastBoot() {
        return this.lastBoot;
    }

    /**
     * Getter for lastBoot as a date
     *
     * @return lastBoot time as a Date object
     */
    public Date getLastBootAsDate() {
        return new Date(this.lastBoot.longValue() * 1000);
    }

    /**
     * Setter for lastBoot
     *
     * @param lastBootIn to set
     */
    public void setLastBoot(Long lastBootIn) {
        this.lastBoot = lastBootIn;
    }

    /**
     * @return Returns the serverArch.
     */
    public ServerArch getServerArch() {
        return serverArch;
    }

    /**
     * @param s The serverArch to set.
     */
    public void setServerArch(ServerArch s) {
        this.serverArch = s;
    }

    /**
     * @return Returns the provisionState.
     */
    public ProvisionState getProvisionState() {
        return provisionState;
    }

    /**
     * @param p The provisionState to set.
     */
    public void setProvisionState(ProvisionState p) {
        this.provisionState = p;
    }

    /**
     * Getter for channelsChanged
     *
     * @return Date to get
     */
    public Date getChannelsChanged() {
        return this.channelsChanged;
    }

    /**
     * Setter for channelsChanged
     *
     * @param channelsChangedIn to set
     */
    public void setChannelsChanged(Date channelsChangedIn) {
        this.channelsChanged = channelsChangedIn;
    }

    /**
     * The set of ServerGroup(s) that this Server is a member of
     * @return Returns the serverGroups.
     */
    public List<EntitlementServerGroup> getEntitledGroups() {
        return ServerGroupFactory.listEntitlementGroups(this);
    }

    /**
     * The set of ServerGroup(s) that this Server is a member of
     * @return Returns the serverGroups.
     */
    public List<ManagedServerGroup> getManagedGroups() {
        return ServerGroupFactory.listManagedGroups(this);
    }

    /**
     * Returns the set of devices attached to this server.
     * @return Returns the list of devices attached to this server.
     */
    public Set<Device> getDevices() {
        return devices;
    }
    /**
     * Sets the set of devices.
     * @param devicesIn The devices to set.
     */
    protected void setDevices(Set<Device> devicesIn) {
        devices = devicesIn;
    }

    /**
     * Get the Device with the given description (i.e. eth0)
     * @param dev the device name (i.e. sda)
     * @return the Device, otherwise null
     */
    public Device getDevice(String dev) {
        for (Device d : getDevices()) {
            if ((d.getDevice() != null) && (d.getDevice().equals(dev))) {
                return d;
            }
        }
        return null;
    }

    /**
     * Adds a device to the list of devices for this server.
     * @param device Device to add
     */
    public void addDevice(Device device) {
        device.setServer(this);
        devices.add(device);
    }

    /**
     * @return Returns the notes.
     */
    public Set<Note> getNotes() {
        return notes;
    }

    /**
     * @param n The notes to set.
     */
    public void setNotes(Set<Note> n) {
        this.notes = n;
    }

    /**
     * Adds a note to the notes set
     * @param note The note to add
     */
    public void addNote(Note note) {
        note.setServer(this);
        notes.add(note);
    }

    /**
     * Adds a note to the notes set.
     * @param user The user creating the note
     * @param subject The subject for the note
     * @param body The body for the note
     */
    public void addNote(User user, String subject, String body) {
        Note note = new Note();
        note.setCreator(user);
        note.setSubject(subject);
        note.setNote(body);
        note.setCreated(new Date());

        addNote(note);
    }

    /**
     * @return Returns the networks
     */
    public Set<Network> getNetworks() {
        return networks;
    }

    /**
     * Sets the set of networks
     * @param n The networks to set
     */
    public void setNetworks(Set<Network> n) {
        this.networks = n;
    }

    /**
     * Adds a network to the set of networks for this server.
     * @param network The network to add.
     */
    public void addNetwork(Network network) {
        network.setServer(this);
        networks.add(network);
    }

    /**
     * Get the primary ip address for this server
     * @return Returns the primary ip for this server
     */
    public String getIpAddress() {
        Network n = findPrimaryNetwork();
        if (n != null) {
            log.debug("Found a Network: " + n.getIpaddr());
            return n.getIpaddr();
        }
        NetworkInterface ni = findPrimaryNetworkInterface();
        if (ni != null) {
            log.debug("Found a NetworkInterface: " + ni.getIpaddr());
            return ni.getIpaddr();
        }
        return null;
    }

    /**
     * Get the primary ipv6 address for this server
     * @return Returns the primary ip for this server
     */
    public String getIp6Address() {
        Network n = findPrimaryIpv6Network();
        if (n != null) {
            log.debug("Found a Network: " + n.getIp6addr());
            return n.getIp6addr();
        }
        return null;
    }


    /**
     * Return the NetworkInterface which Spacewalk is guessing is
     * the primary.  Order of preference:
     *
     * eth0, eth0*, eth1, eth1*, after that its first match that is
     * not 127.0.0.1
     *
     * @return NetworkInterface in order of preference: eth0, eth0*,
     * eth1, eth1*, after that its first match that is not 127.0.0.1
     */
    public NetworkInterface findPrimaryNetworkInterface() {
        primaryInterface = lookupForPrimaryInterface();
        if (primaryInterface != null) {
            return primaryInterface;
        }
        if (!networkInterfaces.isEmpty()) {
            Iterator<NetworkInterface> i = networkInterfaces.iterator();
            // First pass look for names
            NetworkInterface ni = null;

            ni = findActiveIfaceWithName("eth0", false);
            if (ni != null) {
                primaryInterface = ni;
                return ni;
            }
            ni = findActiveIfaceWithName("eth0", true);
            if (ni != null) {
                primaryInterface = ni;
                return ni;
            }
            ni = findActiveIfaceWithName("eth1", false);
            if (ni != null) {
                primaryInterface = ni;
                return ni;
            }
            ni = findActiveIfaceWithName("eth1", true);
            if (ni != null) {
                primaryInterface = ni;
                return ni;
            }
            // Second pass look for localhost
            i = networkInterfaces.iterator();
            while (i.hasNext()) {
                NetworkInterface n = i.next();
                String addr = n.getIpaddr();
                if (addr != null &&
                        !addr.equals("127.0.0.1")) {
                    log.debug("Found NetworkInterface !localhost");
                    primaryInterface = n;
                    return n;
                }
                for (ServerNetAddress6 ad6 : n.getIPv6Addresses()) {
                    if (ad6 != null && !ad6.getAddress().equals("::1")) {
                        log.debug("Found NetworkInterface !localhost");
                        primaryInterface = n;
                        return n;
                    }
                }
            }
            // If we didnt match any of the above criteria
            // just give up and return the 1st one.
            log.debug("just returning 1st network interface");
            primaryInterface = networkInterfaces.iterator().next();
            return primaryInterface;
        }
        primaryInterface = null;
        return null;
    }

    private NetworkInterface findActiveIfaceWithName(String pattern, boolean startsWith) {
        if (networkInterfaces.isEmpty()) {
            return null;
        }
        for (Iterator<NetworkInterface> i = networkInterfaces.iterator(); i
                .hasNext();) {
            NetworkInterface ni = i.next();
            if (ni.isDisabled()) {
                continue;
            }
            if (startsWith) {
                if (ni.getName().startsWith(pattern)) {
                    log.debug("Found " + pattern + "*");
                    return ni;
                }
            }
            else {
                if (ni.getName().equals(pattern)) {
                    log.debug("Found " + pattern);
                    return ni;
                }
            }
        }
        return null;
    }

    // Sometimes java really annoys me
    private Network findPrimaryNetwork() {
        if (!networks.isEmpty()) {
            Iterator<Network> i = networks.iterator();
            while (i.hasNext()) {
                Network n = i.next();
                String addr = n.getIpaddr();
                if (addr != null &&
                        !addr.equals("127.0.0.1")) {
                    log.debug("returning Network that is !localhost");
                    return n;
                }
            }
            log.debug("giving up, returning 1st Network");
            return networks.iterator().next();
        }
        return null;
    }

    private Network findPrimaryIpv6Network() {
        if (!networks.isEmpty()) {
            Iterator<Network> i = networks.iterator();
            while (i.hasNext()) {
                Network n = i.next();
                String addr = n.getIp6addr();
                if (addr != null &&
                        !addr.equals("::1")) {
                    log.debug("returning Network that is !localhost");
                    return n;
                }
            }
            log.debug("giving up, returning 1st Network");
            return networks.iterator().next();
        }
        return null;
    }

    /**
     * Get the primary MAC/hardware address for this server
     * @return Returns the primary MAC/hardware for this server
     */
    public String getHardwareAddress() {
        NetworkInterface network = findPrimaryNetworkInterface();
        if (network != null) {
            return network.getHwaddr();
        }
        return null;
    }

    /**
     * Get the primary hostname for this server
     * @return Returns the primary hostname for this server
     */
    public String getHostname() {
        if (!networks.isEmpty()) {
            Network net = networks.iterator().next();
            return net.getHostname();
        }
        return null;
    }

    /**
     * Get the hostname aliases for this server
     * @return Returns the hostname aliases for this server
     */
    public List<String> getCnames() {
        List<String> result = new ArrayList<String>();
        List<String> proxyCnames = Config.get().getList(
                VALID_CNAMES +
                serverInfo.getId().toString());
        if (!proxyCnames.isEmpty()) {
            result.addAll(proxyCnames);
        }
        return result;
    }

    /**
     * Get the primary hostname for this server
     * If hostname is IDN, it is decoded from Puny encoding
     * @return Returns the primary hostname for this server
     */
    public String getDecodedHostname() {
        String hostname = getHostname();
        return (hostname == null) ? null : IDN.toUnicode(hostname);
    }

    /**
     * Get the hostname aliases (cname records) for this server
     * If hostname is IDN, it is decoded from Puny encoding
     * @return Returns the primary hostname for this server
     */
    public List<String> getDecodedCnames() {
        List<String> result = new ArrayList<String>();
        for (String hostname : getCnames()) {
            result.add(IDN.toUnicode(hostname));
        }
        return result;
    }

    /**
     * @return Returns the networkInterfaces.
     */
    public Set<NetworkInterface> getNetworkInterfaces() {
        return networkInterfaces;
    }

    /**
     * @param n The networkInterfaces to set.
     */
    public void setNetworkInterfaces(Set<NetworkInterface> n) {
        this.networkInterfaces = n;
    }

    /**
     * Adds a network interface to the set of network interfaces
     * for this server.
     * @param netIn The NetworkInterface to add
     */
    public void addNetworkInterface(NetworkInterface netIn) {
        netIn.setServer(this);
        networkInterfaces.add(netIn);
    }

    /**
     * Returns the total amount of ram for this server.
     * @return the total amount of ram for this server.
     */
    public long getRam() {
        if (ram == null) {
            return 0;
        }
        return ram.getRam();
    }

    /**
     * Convenience method for formatting the Ram as a String value.
     * @return String of RAM.
     */
    public String getRamString() {
        return new Long(getRam()).toString();
    }

    /**
     * the total amount of ram for this server.
     * @param ramIn The ram to set.
     */
    public void setRam(long ramIn) {
        initializeRam();
        ram.setRam(ramIn);
    }

    /**
     * Returns the  amount of swap for this server.
     * @return the  amount of swap for this server.
     */
    public long getSwap() {
        if (ram == null) {
            return 0;
        }
        return ram.getSwap();
    }

    /**
     * the amount of swap for this server.
     * @param swapIn the amount of swap for this server.
     */
    public void setSwap(long swapIn) {
        initializeRam();
        ram.setSwap(swapIn);
    }

    /**
     * @return Returns the cpu.
     */
    public CPU getCpu() {
        return cpu;
    }

    /**
     * @param cpuIn The cpu to set.
     */
    public void setCpu(CPU cpuIn) {
        this.cpu = cpuIn;
    }

    /**
     * @return Returns the dmi.
     */
    public Dmi getDmi() {
        return dmi;
    }

    /**
     * @param dmiIn The dmi to set.
     */
    public void setDmi(Dmi dmiIn) {
        dmi = dmiIn;
    }

    /**
     * @return Returns the serverLocation associated with the server.
     */
    public Location getLocation() {
        return serverLocation;
    }

    /**
     * @param locationIn Location to associate with the server.
     */
    public void setLocation(Location locationIn) {
        serverLocation = locationIn;
    }

    private void initializeRam() {
        if (ram == null) {
            ram = new Ram();
            ram.setServer(this);
        }
    }

    /**
     * @return Returns the customDataValues.
     */
    public Set<CustomDataValue> getCustomDataValues() {
        return customDataValues;
    }

    /**
     * @param customDataValuesIn The customDataValues to set.
     */
    public void setCustomDataValues(Set<CustomDataValue> customDataValuesIn) {
        this.customDataValues = customDataValuesIn;
    }

    /**
     * Adds a custom data value to the set of custom data values
     * for this server.
     * @param value The CustomDataValue to add
     */
    public void addCustomDataValue(CustomDataValue value) {
        value.setServer(this);
        customDataValues.add(value);
    }

    /**
     * Adds a custom data value to the set of custom data values
     * @param key The CustomDataKey for this value
     * @param value The value to set
     * @param user The user doing the setting
     */
    public void addCustomDataValue(CustomDataKey key, String value, User user) {
        // Check for null key values.
        if (key == null || key.getLabel() == null) {
            throw new
            UndefinedCustomDataKeyException("CustomDataKey can not be null.");
        }

        // Make sure this org has this particular CustomDataKey defined
        if (!org.hasCustomDataKey(key.getLabel())) {
            throw new
            UndefinedCustomDataKeyException("CustomDataKey: " + key.getLabel() +
                    " is not defined for this org.");
        }

        // get the CustomDataValue
        CustomDataValue customValue = getCustomDataValue(key);

        // does the server already have this key defined?
        if (customValue == null) {
            // create a new CustomDataValue object
            customValue = new CustomDataValue();
            customValue.setCreator(user);
            customValue.setKey(key);
        }
        customValue.setValue(value);
        customValue.setLastModifier(user);
        // add customValue to customDataValues set
        addCustomDataValue(customValue);
    }

    /**
     * Adds a custom data value to the set of custom data values
     * @param keyLabel The label for the CustomDataKey for this value
     * @param value The value to set
     * @param user The user doing the setting
     */
    public void addCustomDataValue(String keyLabel, String value, User user) {
        // look up CustomDataKey by keyLabel
        CustomDataKey key = OrgFactory.lookupKeyByLabelAndOrg(keyLabel, user.getOrg());
        addCustomDataValue(key, value, user);
    }

    /**
     * Retrieves a specific CustomDataValue from the customDataValues set
     * @param key The Key for the value you're looking up
     * @return Returns a CustomDataValue if it exists for this server. null otherwise.
     */
    public CustomDataValue getCustomDataValue(CustomDataKey key) {
        return ServerFactory.getCustomDataValue(key, this);
    }

    /**
     * Returns the set of Channels this Server is subscribed to.
     * @return the set of Channels this Server is subscribed to.
     */
    public Set<Channel> getChannels() {
        return channels;
    }

    protected void setChannels(Set<Channel> chans) {
        channels = chans;
    }

    /**
     * Adds the given channel to this Server.
     * @param c Channel to be added.
     */
    public void addChannel(Channel c) {
        channels.add(c);
    }

    /**
     * Returns the base channel for this server or null if not set.
     * @return Returns the base channel for this server or null if not set.
     */
    public Channel getBaseChannel() {
        /*
         * The base channel for a given server is designated in the database by
         * parent_channel == null. Since the number of channels for a given server is
         * relatively small, loop through the channels set and look for one without a
         * parentChannel instead of going back to the db.
         */
        for (Iterator<Channel> itr = channels.iterator(); itr.hasNext();) {
            Channel channel = itr.next();
            if (channel.getParentChannel() == null) {
                // This is the base channel
                return channel;
            }
        }
        // Either we have no channels or all channels have a parent_channel. In either
        // case, the base channel cannot be determined for this server.
        return null;
    }

    /**
     * Returns true if this is a satellite server.
     * @return true if this is a satellite server.
     */
    public boolean isSatellite() {
        return false;
    }

    /**
     * Returns true if this is a proxy server.
     * @return true if this is a proxy server.
     */
    public boolean isProxy() {
        return getProxyInfo() != null;
    }

    /**
     * Returns true if the server has the given Entitlement.
     * @param entitlement Entitlement to verify.
     * @return true if the server has the given Entitlement.
     */
    public boolean hasEntitlement(Entitlement entitlement) {
        List<?> grps = getEntitledGroups();
        for (Iterator<?> itr = grps.iterator(); itr.hasNext();) {
            ServerGroup g = (ServerGroup) itr.next();

            // The server's group type can be null if the user has created some
            // custom server groups.  If so, we won't check it against the
            // given entitlement.

            ServerGroupType groupType = g.getGroupType();
            if (groupType.getLabel().equals(entitlement.getLabel())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Give a set of the entitlements a server has.
     * This is entirely based on the server groups, but server
     * groups also contain user defined groups.
     * @return a set of Entitlement objects
     */
    public Set<Entitlement> getEntitlements() {
        Set<Entitlement> entitlements = new HashSet<Entitlement>();

        Iterator<EntitlementServerGroup> i = getEntitledGroups().iterator();
        while (i.hasNext()) {
            ServerGroup grp = i.next();
            entitlements.add(EntitlementManager.getByName(
                    grp.getGroupType().getLabel()));
        }
        return entitlements;
    }

    /**
     * Base entitlement for the Server.
     * @return Entitlement that is the base entitlement for the server
     */
    public Entitlement getBaseEntitlement() {
        Entitlement baseEntitlement = null;
        Iterator<EntitlementServerGroup> i = getEntitledGroups().iterator();

        while (i.hasNext() && baseEntitlement == null) {
            ServerGroupType sgt = (i.next()).getGroupType();

            if (sgt.isBase()) {
                baseEntitlement = EntitlementManager.getByName(sgt.getLabel());
            }
        }

        return baseEntitlement;
    }

    /**
     * Base entitlement for the Server.
     * @param baseIn to update to
     */
    public void setBaseEntitlement(Entitlement baseIn) {
        ServerGroupType verify = ServerFactory.
                lookupServerGroupTypeByLabel(baseIn.getLabel());
        if (!verify.isBase()) {
            throw new IllegalArgumentException("baseIn is not a base entitlement");
        }

        Entitlement baseEntitlement = this.getBaseEntitlement();
        if (baseEntitlement != null && baseIn.equals(baseEntitlement)) {
            // noop if there is no change
            return;
        }
        if (baseEntitlement != null) {
            this.getEntitlements().remove(baseEntitlement);
            SystemManager.removeServerEntitlement(this.getId(), baseEntitlement);
        }

        SystemManager.entitleServer(this, baseIn);
    }

    /**
     * Set of add-on entitlements for the Server.
     * @return Set of entitlements that are add-on entitlements for the server
     */
    public Set<Entitlement> getAddOnEntitlements() {
        Set<Entitlement> s = new HashSet<Entitlement>();

        Iterator<?> i = getEntitledGroups().iterator();

        while (i.hasNext()) {
            ServerGroupType sgt = ((ServerGroup) i.next()).getGroupType();

            if (!sgt.isBase()) {
                s.add(EntitlementManager.getByName(sgt.getLabel()));
            }
        }

        return s;
    }

    /**
     * Returns a comma-delimted list of add-on entitlements with their human readable
     * labels.
     *
     * @return A comma-delimted list of add-on entitlements with their human readable
     * labels.
     */
    public String getAddOnEntitlementsAsText() {
        Set<?> addOnEntitlements = getAddOnEntitlements();
        Iterator<?> iterator = addOnEntitlements.iterator();
        StringBuilder buffer = new StringBuilder();
        Entitlement entitlement = null;

        while (iterator.hasNext()) {
            entitlement = (Entitlement)iterator.next();
            buffer.append(entitlement.getHumanReadableLabel()).append(", ");
        }

        if (!addOnEntitlements.isEmpty()) {
            buffer.delete(buffer.length() - 2, buffer.length());
        }

        return buffer.toString();
    }

    /**
     * Return <code>true</code> if this is a virtual host, <code>false</code> otherwise.
     * If this is a host system, {@link #getVirtualInstance()} will always be <code>null
     * </code> since we are not supporting/implementing guests of guest in the RHN 500
     * release.
     *
     * @return true if the system is a virtual host
     */
    public boolean isVirtualHost() {
        return (SystemManager.isVirtualHost(getOrg().getId(), getId())) ||
                hasVirtualizationEntitlement();
    }

    /**
     * Return <code>true</code> if this a guest system, <code>false</code> otherwise. If
     * this system is a guest, {@link #getVirtualInstance()} will be non-<code>null</code>.
     *
     * @return <code>true</code> if this a guest system, <code>false</code> otherwise.
     */
    public boolean isVirtualGuest() {
        return getVirtualInstance() != null;
    }

    /**
     * Return <code>true</code> if this system has virtualization entitlement,
     * <code>false</code> otherwise.
     * @return <code>true</code> if this system has virtualization entitlement,
     *      <code>false</code> otherwise.
     */
    public boolean hasVirtualizationEntitlement() {
        return hasEntitlement(EntitlementManager.VIRTUALIZATION);
    }

    /**
     *
     * @return the virtual guests
     */
    private Set<VirtualInstance> getVirtualGuests() {
        return guests;
    }

    private void setVirtualGuests(Set<VirtualInstance> virtualGuests) {
        this.guests = virtualGuests;
    }

    /**
     * Returns a read-only collection of VirtualInstance objects.
     * @return A read-only collection of VirtualInstance objects.
     */
    public Collection<VirtualInstance> getGuests() {
        Set<VirtualInstance> retval = new HashSet<VirtualInstance>();
        for (VirtualInstance vi : getVirtualGuests()) {
            // Filter out the hosts that sometimes show up in this table.
            // Hosts have no UUID defined.
            if (vi.getUuid() != null) {
                retval.add(vi);
            }
        }
        return Collections.unmodifiableCollection(retval);
    }

    /**
     *
     * @param guest the guest to add
     */
    public void addGuest(VirtualInstance guest) {
        guest.setHostSystem(this);
        guests.add(guest);
    }

    /**
     * Removes the virtual instance guest from this server. If the guest is registered,
     * then the guest server will be deleted from the virtual instance.
     *
     * @param guest The virtual instance to delete
     *
     * @return <code>true</code> if the guest is deleted, <code>false</code> otherwise.
     */
    public boolean deleteGuest(VirtualInstance guest) {
        if (canDeleteGuest(guest)) {
            guest.deleteGuestSystem();
            return removeGuest(guest);
        }
        return false;
    }

    private boolean canDeleteGuest(VirtualInstance guest) {
        for (VirtualInstance g : getGuests()) {
            if (g.getId().equals(guest.getId())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Remove the association between a guest and this server, but do not delete the
     * guest server.
     *
     * @param guest Guest to remove from this server.
     * @return <code>true</code> if the guest is deleted, <code>false</code> otherwise.
     */
    public boolean removeGuest(VirtualInstance guest) {
        boolean deleted = false;
        for (Iterator<VirtualInstance> it = guests.iterator(); it.hasNext();) {
            VirtualInstance g = it.next();
            if (g.getId().equals(guest.getId())) {
                guest.setHostSystem(null);

                it.remove();
                deleted = true;
                break;
            }
        }

        return deleted;
    }


    /**
     * Return the virtual instance that owns this server when the server is a virtual guest.
     *
     * @return The virtual instance that owns this server when the server is a virtual
     * guest. If the server is not a guest, the method returns <code>null</code>.
     */
    public VirtualInstance getVirtualInstance() {
        return virtualInstance;
    }

    /**
     * Sets the owning virtual instance for this server, which effectively makes this a
     * guest system.
     *
     * @param instance The owning virtual instance
     */
    // Note that while the relationship between guest and virtual instance needs to be
    // bi-directional, we want to manage the relationship (add/delete) from the virtual
    // instance since it is the owner/parent. Hence, the reason for package visibility on
    // this method.
    void setVirtualInstance(VirtualInstance instance) {
        virtualInstance = instance;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(final Object other) {
        if (other == null || !(other instanceof Server)) {
            return false;
        }
        Server castOther = (Server) other;

        return new EqualsBuilder().append(os, castOther.getOs())
                .append(release, castOther.getRelease())
                .append(name, castOther.getName())
                .append(description, castOther.getDescription())
                .append(info, castOther.getInfo())
                .append(secret, castOther.getSecret())
                .append(autoUpdate, castOther.getAutoUpdate())
                .append(runningKernel, castOther.getRunningKernel())
                .append(lastBoot, castOther.getLastBoot())
                .append(channelsChanged, castOther.getChannelsChanged())
                .append(getProxyInfo(), castOther.getProxyInfo())
                .isEquals();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(id).append(digitalServerId).append(os)
                .append(release).append(name).append(description)
                .append(info).append(secret)
                .append(autoUpdate).append(runningKernel)
                .append(lastBoot).append(channelsChanged).
                append(getProxyInfo())
                .toHashCode();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE).append(
                "id", id).append("org", org).append("name", name).append(
                        "description", description).toString();
    }

    /**
     * @return Returns the created.
     */
    @Override
    public Date getCreated() {
        return created;
    }

    /**
     * @param createdIn The created to set.
     */
    @Override
    public void setCreated(Date createdIn) {
        this.created = createdIn;
    }

    /**
     * @return Returns the lock.
     */
    public ServerLock getLock() {
        return lock;
    }

    /**
     * @param lockIn The lock to set.
     */
    public void setLock(ServerLock lockIn) {
        this.lock = lockIn;
    }

    /**
     * @return Returns the uuid.
     */
    public ServerUuid getServerUuid() {
        return this.serverUuid;
    }

    /**
     * @param serverUuidIn The uuid to set.
     */
    public void setServerUuid(ServerUuid serverUuidIn) {
        this.serverUuid = serverUuidIn;
    }

    /**
     * Business method to check if the system is considered 'inactive'
     * @return boolean if it hasn't checked in recently.
     */
    public boolean isInactive() {
        Date lastCheckin = this.getLastCheckin();
        long millisInDay = (1000 * 60 * 60 * 24);
        long threshold = Config.get().getInt(ConfigDefaults.SYSTEM_CHECKIN_THRESHOLD, 1);
        Date yesterday = new Timestamp(System.currentTimeMillis() -
                (millisInDay * threshold));
        return lastCheckin.before(yesterday);
    }


    /**
     * Get the Set of Child Channel objects associated with this server.  This
     * is just a convenience method.  Basically the channels associated with this
     * server that are not base channels.
     *
     * @return Set of Child Channels.  null of none found.
     */
    public Set<Channel> getChildChannels() {
        // Make sure we return NULL if none are found
        if (this.getChannels() != null) {
            Set<Channel> retval = new HashSet<Channel>();
            for (Channel c : this.getChannels()) {
                // add non base channels (children)
                // to return set.
                if (!c.isBaseChannel()) {
                    retval.add(c);
                }
            }
            if (retval.size() == 0) {
                return new HashSet<Channel>();
            }
            return retval;
        }
        return new HashSet<Channel>();
    }

    /**
     * @return The push client for this server.
     */
    public PushClient getPushClient() {
        return pushClient;
    }

    /**
     * @param pushClientIn The push client to be used for this server.
     */
    public void setPushClient(PushClient pushClientIn) {
        this.pushClient = pushClientIn;
    }

    /**
     * Simple check to see if the Server is subscribed to the passed in channel already.
     * @param channelIn to check
     * @return boolean true false if subbed or not.
     */
    public boolean isSubscribed(Channel channelIn) {
        Set<Channel> childChannels = this.channels;
        if (childChannels != null) {
            return childChannels.contains(channelIn);
        }
        return false;
    }

    /**
     * Check to see if the passed in entitlement can be applied to this server.
     * @param entIn to check
     * @return boolean if its compatible with this server.
     */
    public boolean isEntitlementAllowed(Entitlement entIn) {
        // Check virt entitlements.
        if (this.isVirtualGuest()) {
            if (entIn instanceof VirtualizationEntitlement) {
                return false;
            }
        }
        return true;
    }

    /**
     * Get the Set of valid addon Entitlements for this server.
     *
     * @return Set of valid addon Entitlement instances for this server
     */
    public Set<Entitlement> getValidAddonEntitlementsForServer() {
        Set<Entitlement> retval = new TreeSet<Entitlement>();
        Iterator<?> i = this.getOrg().getValidAddOnEntitlementsForOrg()
                .iterator();
        while (i.hasNext()) {
            Entitlement ent = (Entitlement) i.next();
            if (this.isEntitlementAllowed(ent)) {
                retval.add(ent);
            }
        }
        return retval;

    }

    /**
     * @return this list of history events for this server
     */
    public Set<ServerHistoryEvent> getHistory() {
        return history;
    }

    /**
     * Set the history events for this server
     * @param historyIn the List of history events
     */
    public void setHistory(Set<ServerHistoryEvent> historyIn) {
        this.history = historyIn;
    }

    /**
     * @return Returns the packages.
     */
    public Set<InstalledPackage> getPackages() {
        return packages;
    }


    /**
     * @param packagesIn The packages to set.
     */
    public void setPackages(Set<InstalledPackage> packagesIn) {
        this.packages = packagesIn;
    }

    /**
     * @return Returns the cobblerId.
     */
    public String getCobblerId() {
        return cobblerId;
    }

    /**
     * @param cobblerIdIn The cobblerId to set.
     */
    public void setCobblerId(String cobblerIdIn) {
        this.cobblerId = cobblerIdIn;
    }

    /**
     * @return Returns the ignoreEntitlementsForMigration.
     */
    public Boolean getIgnoreEntitlementsForMigration() {
        return ignoreEntitlementsForMigration;
    }

    /**
     * This method should ONLY be used for system migrations, hence the long method name.
     *
     * This method will set a local flag (i.e. not Hibernate-related) that if set will
     * result in skipping entitlement checking on various methods.
     *
     * @param ignoreIn  Set to true to override entitlement sestings.
     */
    public void setIgnoreEntitlementsForMigration(Boolean ignoreIn) {
        this.ignoreEntitlementsForMigration = ignoreIn;
    }

    /**
     * Get the NetworkInteface with the given name (i.e. eth0)
     * @param ifName the interface name (i.e. eth0)
     * @return the NetworkInterface, otherwise null
     */
    public NetworkInterface getNetworkInterface(String ifName) {
        for (NetworkInterface nic : getNetworkInterfaces()) {
            if (nic.getName().equals(ifName)) {
                return nic;
            }
        }
        return null;
    }

    /**
     * Returns the cobbler object associated to
     * to this server.
     * @param user the user object needed for connection,
     *              enter null if you want to use the
     *              automated connection as provided by
     *              taskomatic.
     * @return the SystemRecord associated to this server
     */
    public SystemRecord getCobblerObject(User user) {
        if (StringUtils.isBlank(getCobblerId())) {
            return null;
        }
        CobblerConnection con;
        if (user == null) {
            con = CobblerXMLRPCHelper.getAutomatedConnection();
        }
        else {
            con = CobblerXMLRPCHelper.getConnection(user);
        }
        return SystemRecord.lookupById(con, getCobblerId());
    }

    /**
     * @return Return application crashes.
     */
    public CrashCount getCrashCount() {
        return crashCount;
    }

    /**
     * @param crashIn Set application crashes.
     */
    public void setCrashCount(CrashCount crashIn) {
        crashCount = crashIn;
    }

    /**
     * @return primaryInterface Primary network interface
     */
    public NetworkInterface getPrimaryInterface() {
        return primaryInterface;
    }

    /**
     * @param primaryInterfaceIn Primary network interface to be set
     */
    public void setPrimaryInterface(NetworkInterface primaryInterfaceIn) {
        primaryInterface = primaryInterfaceIn;
        Iterator<NetworkInterface> i = networkInterfaces.iterator();
        while (i.hasNext()) {
            NetworkInterface n = i.next();
            n.setPrimary(null);
        }
        SystemManager.storeServer(this);
        primaryInterface.setPrimary("Y");
        if (networks.size() == 1) {
            Network n = networks.iterator().next();
            n.setIpaddr(primaryInterface.getIpaddr());
            n.setIp6addr(primaryInterface.getGlobalIpv6Addr());
        }
    }

    /**
     * @param interfaceName name of the interface
     */
    public void setPrimaryInterfaceWithName(String interfaceName) {
        setPrimaryInterface(findActiveIfaceWithName(interfaceName, false));
    }

    private NetworkInterface lookupForPrimaryInterface() {
        for (NetworkInterface n : networkInterfaces) {
            if (n.getPrimary() != null && n.getPrimary().equals("Y")) {
                return n;
            }
        }
        return null;
    }

    /**
     * @return active Set of active interaces without lo
     */
    public Set <NetworkInterface> getActiveNetworkInterfaces() {
        Set <NetworkInterface> active = new HashSet();
        for (NetworkInterface n : networkInterfaces) {
            if (!n.isDisabled()) {
                active.add(n);
            }
        }
        return active;
    }

    /**
     * @return Returns the crashes.
     */
    public Set<Crash> getCrashes() {
        return crashes;
    }

    /**
     * @param c The crashes to set.
     */
    public void setCrashes(Set<Crash> c) {
        this.crashes = c;
    }

    /**
     * @param interfaceName Name of the interface to be checked
     * @return Returns true if yes, otherwise no
     */
    public Boolean existsActiveInterfaceWithName(String interfaceName) {
        return findActiveIfaceWithName(interfaceName, false) != null;
    }
}
