/*
 * Copyright 2017-2023 Open Networking Foundation (ONF) and the ONF Contributors
 *
 * 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.opencord.aaa.impl;

import org.onlab.packet.RADIUS;
import org.onlab.packet.RADIUSAttribute;
import org.onosproject.event.AbstractListenerManager;
import org.opencord.aaa.RadiusCommunicator;
import org.opencord.aaa.RadiusOperationalStatusEvent;
import org.opencord.aaa.RadiusOperationalStatusEventDelegate;
import org.opencord.aaa.RadiusOperationalStatusEventListener;
import org.opencord.aaa.RadiusOperationalStatusService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.slf4j.Logger;

import static org.slf4j.LoggerFactory.getLogger;

@Component(immediate = true)
public class RadiusOperationalStatusManager
        extends AbstractListenerManager<RadiusOperationalStatusEvent, RadiusOperationalStatusEventListener>
        implements RadiusOperationalStatusService {

    private byte[] address;
    private String secret;
    private RadiusCommunicator impl;
    private RadiusOperationalStatusEventDelegate radiusOprStDelegate;

    private long operationalStatusServerTimeoutInMillis;
    private boolean statusServerReqSent;
    private final Logger log = getLogger(getClass());

    private Boolean fakeAccessRequestPacketRecieved = false;
    private long fakeAccessRequestOutTimeInMillis;

    private Boolean serverStatusPacketRecieved = false;
    private long serverStatusOutTimeInMillis;

    private OperationalStatus radiusServerOperationalStatus;

    public static final byte AAA_REQUEST_ID_STATUS_REQUEST = 0;
    public static final byte AAA_REQUEST_ID_FAKE_ACCESS_REQUEST = 1;

    private RadiusOperationalStatusEvaluationMode radiusOperationalStatusEvaluationMode;

    // FIXME the DUMMY_USER should be read from config (or netcfg)
    private static final String DUMMY_USER = new String("user");
    private static final byte RADIUS_CODE_STATUS_REQUEST = (byte) 12;
    private long lastRadiusPacketInTimeInMillis;

    public void setOperationalStatusServerTimeoutInMillis(long operationalStatusServerTimeoutInMillis) {
        this.operationalStatusServerTimeoutInMillis = operationalStatusServerTimeoutInMillis;
    }

    public void setRadiusOperationalStatusEvaluationMode(
        RadiusOperationalStatusEvaluationMode radiusOperationalStatusEvaluationMode) {
        this.radiusOperationalStatusEvaluationMode = radiusOperationalStatusEvaluationMode;
    }

    public RadiusOperationalStatusEventDelegate getRadiusOprStDelegate() {
        return radiusOprStDelegate;
    }

    @Override
    public void setOutTimeInMillis(byte identifier) {
        if (identifier == AAA_REQUEST_ID_STATUS_REQUEST) {
            serverStatusOutTimeInMillis = System.currentTimeMillis();
        } else {
            fakeAccessRequestOutTimeInMillis = System.currentTimeMillis();
        }
    }

    @Override
    public String getRadiusServerOperationalStatus() {
        return radiusServerOperationalStatus.toString();
    }

    @Activate
    public void activate() {
        radiusOprStDelegate = new InternalRadiusOperationalStatusDelegate();
        eventDispatcher.addSink(RadiusOperationalStatusEvent.class, listenerRegistry);
        radiusServerOperationalStatus = OperationalStatus.UNKNOWN;
    }

    public void setStatusServerReqSent(boolean statusServerReqSent) {
        this.statusServerReqSent = statusServerReqSent;
    }

    @Deactivate
    public void deactivate() {
        eventDispatcher.removeSink(RadiusOperationalStatusEvent.class);
    }

    public void initialize(byte[] address, String secret, RadiusCommunicator impl) {
        this.address = address;
        this.secret = secret;
        this.impl = impl;
    }

    public boolean isRadiusResponseForOperationalStatus(byte identifier) {
        if (identifier == AAA_REQUEST_ID_STATUS_REQUEST || identifier == AAA_REQUEST_ID_FAKE_ACCESS_REQUEST) {
            return true;
        } else {
            lastRadiusPacketInTimeInMillis = System.currentTimeMillis();
            return false;
        }
    }

    public void handleRadiusPacketForOperationalStatus(RADIUS radiusPacket) {
        byte radiusPktIdentifier = radiusPacket.getIdentifier();

        if (radiusPktIdentifier == AAA_REQUEST_ID_STATUS_REQUEST) {
            long serverStatusRttInMillis = System.currentTimeMillis() - serverStatusOutTimeInMillis;
            if (serverStatusRttInMillis < operationalStatusServerTimeoutInMillis) {
                serverStatusPacketRecieved = true;
            }
        } else {
            long fakeAccessRttInMillis = System.currentTimeMillis() - fakeAccessRequestOutTimeInMillis;
            if (fakeAccessRttInMillis < operationalStatusServerTimeoutInMillis) {
                fakeAccessRequestPacketRecieved = true;
            }
        }

        switch (radiusPacket.getCode()) {
            case RADIUS.RADIUS_CODE_ACCESS_ACCEPT:
                synchronized (serverStatusPacketRecieved) {
                    serverStatusPacketRecieved.notify();
                }
                break;
            case RADIUS.RADIUS_CODE_ACCESS_REJECT:
                synchronized (fakeAccessRequestPacketRecieved) {
                    fakeAccessRequestPacketRecieved.notify();
                }
                break;
            default:
                log.warn("Unexpected Radius message for operational status recieved "
                        + "with code: {}", radiusPacket.getCode());
        }
    }

    public void checkServerStatusUsingStatusServerRequest() throws InterruptedException {
        RADIUS radiusStatusServerRequest;
        // identifier = 0 for status server
        radiusStatusServerRequest = new RADIUS(RADIUS_CODE_STATUS_REQUEST, AAA_REQUEST_ID_STATUS_REQUEST);

        radiusStatusServerRequest.setIdentifier(AAA_REQUEST_ID_STATUS_REQUEST);
        radiusStatusServerRequest.setAttribute(RADIUSAttribute.RADIUS_ATTR_USERNAME, DUMMY_USER.getBytes());

        radiusStatusServerRequest.setAttribute(RADIUSAttribute.RADIUS_ATTR_NAS_IP, address);
        radiusStatusServerRequest.addMessageAuthenticator(secret);
        setOutTimeInMillis(radiusStatusServerRequest.getIdentifier());
        impl.sendRadiusPacket(radiusStatusServerRequest, null);
        synchronized (serverStatusPacketRecieved) {
            serverStatusPacketRecieved.wait(operationalStatusServerTimeoutInMillis);
        }
    }

    public void checkServerStatusUsingFakeAccessRequest() throws InterruptedException {
        RADIUS radiusDummyAccessRequest;
        // identifier = 1 for fake accessRequest
        radiusDummyAccessRequest = new RADIUS(RADIUS.RADIUS_CODE_ACCESS_REQUEST, AAA_REQUEST_ID_FAKE_ACCESS_REQUEST);

        radiusDummyAccessRequest.setIdentifier(AAA_REQUEST_ID_FAKE_ACCESS_REQUEST);
        radiusDummyAccessRequest.setAttribute(RADIUSAttribute.RADIUS_ATTR_USERNAME, DUMMY_USER.getBytes());

        radiusDummyAccessRequest.setAttribute(RADIUSAttribute.RADIUS_ATTR_NAS_IP, address);
        radiusDummyAccessRequest.addMessageAuthenticator(secret);
        setOutTimeInMillis(radiusDummyAccessRequest.getIdentifier());
        impl.sendRadiusPacket(radiusDummyAccessRequest, null);
        synchronized (fakeAccessRequestPacketRecieved) {
            fakeAccessRequestPacketRecieved.wait(operationalStatusServerTimeoutInMillis);
        }
    }

    public void checkStatusServerForAccessRequestMode() throws InterruptedException {
        long radiusResponseRecievedTimeDifference = System.currentTimeMillis() - lastRadiusPacketInTimeInMillis;
        if (radiusResponseRecievedTimeDifference > operationalStatusServerTimeoutInMillis) {
            checkServerStatusUsingFakeAccessRequest();
            if (statusServerReqSent && fakeAccessRequestPacketRecieved) {
                radiusServerOperationalStatus = OperationalStatus.IN_USE;
            } else if (statusServerReqSent && !fakeAccessRequestPacketRecieved) {
                radiusServerOperationalStatus = OperationalStatus.UNAVAILABLE;
            } else {
                radiusServerOperationalStatus = OperationalStatus.UNKNOWN;
            }
        } else {
            radiusServerOperationalStatus = OperationalStatus.IN_USE;
        }
    }

    public void checkServerOperationalStatus() {

        try {
            if (radiusOperationalStatusEvaluationMode == RadiusOperationalStatusEvaluationMode.STATUS_REQUEST) {
                // determine operational status by statusServerRequest
                checkServerStatusUsingStatusServerRequest();
                if (statusServerReqSent && serverStatusPacketRecieved) {
                    // if req sent and response recieved
                    radiusServerOperationalStatus = OperationalStatus.IN_USE;
                } else if (statusServerReqSent && !serverStatusPacketRecieved) {
                    radiusServerOperationalStatus = OperationalStatus.UNAVAILABLE;
                } else {
                radiusServerOperationalStatus = OperationalStatus.UNKNOWN;
                }
            } else {
                if (radiusOperationalStatusEvaluationMode == RadiusOperationalStatusEvaluationMode.AUTO) {
                    checkServerStatusUsingStatusServerRequest();
                    if (statusServerReqSent && serverStatusPacketRecieved) {
                        radiusServerOperationalStatus = OperationalStatus.IN_USE;
                    } else {
                        checkStatusServerForAccessRequestMode();
                    }
                } else {
                    checkStatusServerForAccessRequestMode();
                }
            }
            fakeAccessRequestPacketRecieved = false;
            serverStatusPacketRecieved = false;
        } catch (Exception e) {
            log.error("Caught exception while checking radius server status::" + e);
        }
    }

    /**
     * Delegate allowing the RadiusOperationalStatus to notify us of events.
     */
     private class InternalRadiusOperationalStatusDelegate implements RadiusOperationalStatusEventDelegate {
        @Override
        public void notify(RadiusOperationalStatusEvent radiusOperationalStatusEvent) {
            log.debug("Radius Operational Status event {} for {}", radiusOperationalStatusEvent.type(),
                radiusOperationalStatusEvent.subject());
            post(radiusOperationalStatusEvent);
        }
     }

}
