package org.apereo.cas.adaptors.radius;


import net.jradius.client.RadiusClient;
import net.jradius.dictionary.Attr_NASIPAddress;
import net.jradius.dictionary.Attr_NASIPv6Address;
import net.jradius.dictionary.Attr_NASIdentifier;
import net.jradius.dictionary.Attr_NASPort;
import net.jradius.dictionary.Attr_NASPortId;
import net.jradius.dictionary.Attr_NASPortType;
import net.jradius.dictionary.Attr_UserName;
import net.jradius.dictionary.Attr_UserPassword;
import net.jradius.dictionary.vsa_redback.Attr_NASRealPort;
import net.jradius.packet.AccessAccept;
import net.jradius.packet.AccessRequest;
import net.jradius.packet.RadiusPacket;
import net.jradius.packet.attribute.AttributeFactory;
import net.jradius.packet.attribute.AttributeList;
import net.jradius.packet.attribute.RadiusAttribute;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * Implementation of a RadiusServer that utilizes the JRadius packages available
 * at <a href="http://jradius.sf.net">http://jradius.sf.net</a>.
 *
 * @author Scott Battaglia
 * @author Marvin S. Addison
 * @author Misagh Moayyed
 * @since 3.1
 */
public class JRadiusServerImpl implements RadiusServer {

    /**
     * Default retry count, {@value}.
     **/
    public static final int DEFAULT_RETRY_COUNT = 3;

    /** Logger instance. */
    private static final Logger LOGGER = LoggerFactory.getLogger(JRadiusServerImpl.class);

    /** RADIUS protocol. */
    
    private final RadiusProtocol protocol;

    /** Produces RADIUS client instances for authentication. */
    
    private final RadiusClientFactory radiusClientFactory;

    /** Number of times to retry authentication when no response is received. */
    
    private int retries = DEFAULT_RETRY_COUNT;

    private String nasIpAddress;
    
    private String nasIpv6Address;
    
    private long nasPort = -1;
    
    private long nasPortId = -1;
    
    private String nasIdentifier;
    
    private long nasRealPort = -1;
    
    private long nasPortType = -1;
    
    static {
        AttributeFactory.loadAttributeDictionary("net.jradius.dictionary.AttributeDictionaryImpl");
    }

    /**
     * Instantiates a new server implementation
     * with the radius protocol and client factory specified. 
     *
     * @param protocol the protocol
     * @param clientFactory the client factory
     * @param retries the new retries
     * @param nasIpAddress the new nas ip address
     * @param nasIpv6Address the new nas ipv6 address
     * @param nasPort the new nas port
     * @param nasPortId the new nas port id
     * @param nasIdentifier the new nas identifier
     * @param nasRealPort the new nas real port
     */
    public JRadiusServerImpl(final RadiusProtocol protocol, final RadiusClientFactory clientFactory, final int retries, final String nasIpAddress,
                             final String nasIpv6Address, final long nasPort, final long nasPortId, final String nasIdentifier, final long nasRealPort) {
        this.protocol = protocol;
        this.radiusClientFactory = clientFactory;
        this.retries = retries;
        this.nasIpAddress = nasIpAddress;
        this.nasIpv6Address = nasIpv6Address;
        this.nasPort = nasPort;
        this.nasPortId = nasPortId;
        this.nasIdentifier = nasIdentifier;
        this.nasRealPort = nasRealPort;
    }

    @Override
    public RadiusResponse authenticate(final String username, final String password) throws Exception {

        final AttributeList attributeList = new AttributeList();
        
        attributeList.add(new Attr_UserName(username));
        attributeList.add(new Attr_UserPassword(password));

        if (StringUtils.isNotBlank(this.nasIpAddress)) {
            attributeList.add(new Attr_NASIPAddress(this.nasIpAddress));
        }
        if (StringUtils.isNotBlank(this.nasIpv6Address)) {
            attributeList.add(new Attr_NASIPv6Address(this.nasIpv6Address));
        }

        if (this.nasPort != -1) {
            attributeList.add(new Attr_NASPort(this.nasPort));
        }
        if (this.nasPortId != -1) {
            attributeList.add(new Attr_NASPortId(this.nasPortId));
        }
        if (StringUtils.isNotBlank(this.nasIdentifier)) {
            attributeList.add(new Attr_NASIdentifier(this.nasIdentifier));
        }
        if (this.nasRealPort != -1) {
            attributeList.add(new Attr_NASRealPort(this.nasRealPort));
        }
        if (this.nasPortType != -1) {
            attributeList.add(new Attr_NASPortType(this.nasPortType));
        }
        
        RadiusClient client = null;
        try {
            client = this.radiusClientFactory.newInstance();
            final AccessRequest request = new AccessRequest(client, attributeList);
            final RadiusPacket response = client.authenticate(
                    request,
                    RadiusClient.getAuthProtocol(this.protocol.getName()),
                    this.retries);

            LOGGER.debug("RADIUS response from [{}]: [{}]",
                    client.getRemoteInetAddress().getCanonicalHostName(),
                    response.getClass().getName());

            if (response instanceof AccessAccept) {
                final List<RadiusAttribute> attributes = response.getAttributes().getAttributeList();
                LOGGER.debug("Radius response code [{}] accepted with attributes [{}] and identifier [{}]",
                        response.getCode(), attributes, response.getIdentifier());
                
                return new RadiusResponse(response.getCode(),
                        response.getIdentifier(),
                        attributes);
            }
            LOGGER.debug("Response is not recognized");
        } finally {
            if (client != null) {
                client.close();
            }
        }
        return null;
    }

    /**
     * Sets the nas port type.
     *
     * @param nasPortType the new nas port type
     * @since 4.1.0
     */
    public void setNasPortType(final long nasPortType) {
        this.nasPortType = nasPortType;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("protocol", this.protocol)
                .append("radiusClientFactory", this.radiusClientFactory)
                .append("retries", this.retries)
                .append("nasIpAddress", this.nasIpAddress)
                .append("nasIpv6Address", this.nasIpv6Address)
                .append("nasPort", this.nasPort)
                .append("nasPortId", this.nasPortId)
                .append("nasIdentifier", this.nasIdentifier)
                .append("nasRealPort", this.nasRealPort)
                .append("nasPortType", this.nasPortType)
                .toString();
    }
}
