package org.ovirt.engine.core.notifier.transport.snmp;

import java.io.IOException;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.ovirt.engine.core.common.AuditLogType;
import org.ovirt.engine.core.common.EventNotificationMethod;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.notifier.NotificationServiceException;
import org.ovirt.engine.core.notifier.dao.DispatchResult;
import org.ovirt.engine.core.notifier.filter.AuditLogEvent;
import org.ovirt.engine.core.notifier.transport.Transport;
import org.ovirt.engine.core.notifier.utils.NotificationProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.ScopedPDU;
import org.snmp4j.Target;
import org.snmp4j.TransportMapping;
import org.snmp4j.UserTarget;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.security.AuthMD5;
import org.snmp4j.security.AuthSHA;
import org.snmp4j.security.PrivAES128;
import org.snmp4j.security.PrivAES192;
import org.snmp4j.security.PrivAES256;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM;
import org.snmp4j.security.UsmUser;
import org.snmp4j.smi.Integer32;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.TimeTicks;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.DefaultUdpTransportMapping;

public class Snmp extends Transport {

    private static final Logger log = LoggerFactory.getLogger(Snmp.class);

    private static final String SNMP_MANAGERS = "SNMP_MANAGERS";
    private static final String SNMP_COMMUNITY = "SNMP_COMMUNITY";
    private static final String SNMP_ENGINE_ID = "SNMP_ENGINE_ID";
    private static final String SNMP_USERNAME = "SNMP_USERNAME";
    private static final String SNMP_AUTH_PROTOCOL = "SNMP_AUTH_PROTOCOL";
    private static final String SNMP_AUTH_PASSPHRASE = "SNMP_AUTH_PASSPHRASE";
    private static final String SNMP_PRIVACY_PROTOCOL = "SNMP_PRIVACY_PROTOCOL";
    private static final String SNMP_PRIVACY_PASSPHRASE = "SNMP_PRIVACY_PASSPHRASE";
    private static final String SNMP_SECURITY_LEVEL = "SNMP_SECURITY_LEVEL";
    private static final String SNMP_OID = "SNMP_OID";
    private static final String SNMP_VERSION = "SNMP_VERSION";
    private static final int ENTERPRISE_SPECIFIC = 6;
    private static final Pattern PROFILE_PATTERN = Pattern.compile(SNMP_MANAGERS + "(|_(?<profile>.*))");

    /** OIDs - See OVIRT-MIB.txt */
    public static final int AUDIT = 1;

    private static final OID OBJECTS_AUDIT = new OID(new int[] {2, 1});

    public static final int INSTANCE_ID = 1;
    public static final int NAME = 2;
    public static final int ID = 3;
    public static final int SEVERITY = 4;
    public static final int MESSAGE = 5;
    public static final int STATUS = 6;
    public static final int DATETIME = 7;
    public static final int USERNAME = 100;
    public static final int USER_ID = 101;
    public static final int VM_NAME = 102;
    public static final int VM_ID = 103;
    public static final int VDS_NAME = 104;
    public static final int VDS_ID = 105;
    public static final int VM_TEMPLATE_NAME = 106;
    public static final int VM_TEMPLATE_ID = 107;
    public static final int STORAGE_POOL_NAME = 108;
    public static final int STORAGE_POOL_ID = 109;
    public static final int STORAGE_DOMAIN_NAME = 110;
    public static final int STORAGE_DOMAIN_ID = 111;


    private final Map<String, Profile> profiles = new HashMap<>();

    private static final String ISO8601 = "yyyy-MM-dd'T'HH:mm'Z'";
    private boolean active = false;
    private static long nanoStart = System.nanoTime();
    private Map<Profile, org.snmp4j.Snmp> profileSnmpMap = new HashMap<>();

    public Snmp(NotificationProperties props) {
        for (Map.Entry<String, String> entry : props.getProperties().entrySet()) {
            Matcher m = PROFILE_PATTERN.matcher(entry.getKey());
            if (m.matches()) {
                String profile = m.group("profile") == null ? "" : m.group("profile");
                String managers = props.getProperty(SNMP_MANAGERS, profile, false);
                if (!StringUtils.isBlank(managers)) {
                    int snmpVersion = getSnmpVersion(props.getProperty(SNMP_VERSION, false));
                    int securityLevel = Integer.parseInt(props.getProperty(SNMP_SECURITY_LEVEL, profile, false));
                    if (snmpVersion == SnmpConstants.version3) {
                        profiles.put(
                                profile,
                                Profile.buildProfile(
                                        managers,
                                        props.getProperty(SNMP_ENGINE_ID, profile, false),
                                        props.getProperty(SNMP_USERNAME, profile, false),
                                        securityLevel == 1 ?
                                                null :
                                                getAuthProtocol(props.getProperty(SNMP_AUTH_PROTOCOL, profile, false)),
                                        props.getProperty(SNMP_AUTH_PASSPHRASE, profile, securityLevel != 1),
                                        securityLevel == 3 ?
                                                getPrivProtocol(props.getProperty(SNMP_PRIVACY_PROTOCOL, profile, false)) :
                                                null,
                                        props.getProperty(SNMP_PRIVACY_PASSPHRASE, profile, securityLevel != 3),
                                        securityLevel,
                                        props.getProperty(SNMP_OID, profile, false),
                                        snmpVersion
                                )
                        );
                    } else {
                        profiles.put(
                                profile,
                                Profile.buildProfile(
                                        managers,
                                        props.getProperty(SNMP_COMMUNITY, profile, false),
                                        props.getProperty(SNMP_OID, profile, false),
                                        snmpVersion
                                )
                        );
                    }
                }
            }
        }

        if (!profiles.isEmpty()) {
            active = true;
        }
    }

    private int getSnmpVersion(String ver) {
        int version;
        switch(ver) {
            case "2":
                version = SnmpConstants.version2c;
                break;
            case "3":
                version = SnmpConstants.version3;
                break;
            default:
                throw new NotificationServiceException("Unknown SNMP_VERSION in properties file " + ver);
        }
        return version;
    }

    private OID getAuthProtocol(String authProtocol) {
        switch(authProtocol) {
            case "MD5":
                return AuthMD5.ID;
            case "SHA":
                return AuthSHA.ID;
            default:
                throw new NotificationServiceException("Unknown SNMP_AUTH_PROTOCOL in properties file " + authProtocol);
        }
    }

    private OID getPrivProtocol(String privProtocol) {
        switch(privProtocol) {
            case "AES128":
                return PrivAES128.ID;
            case "AES192":
                return PrivAES192.ID;
            case "AES256":
                return PrivAES256.ID;
            default:
                throw new NotificationServiceException("Unknown SNMP_PRIVACY_PROTOCOL in properties file " + privProtocol);
        }
    }

    @Override
    public String getName() {
        return "snmp";
    }

    @Override
    public boolean isActive() {
        return active;
    }

    @Override
    public void dispatchEvent(AuditLogEvent event, String address) {
        Profile profile = profiles.get(address);
        if (profile == null) {
            profile = profiles.get("");
            if (profile == null) {
                log.warn("Could not find snmp profile: {}", address);
                return;
            }
        }

        PDU pdu;
        org.snmp4j.Snmp snmpForProfile = getSnmpForProfile(profile);
        if (profile.version == SnmpConstants.version3) {
            // Create PDU for V3
            pdu = new ScopedPDU();
            ((ScopedPDU) pdu).setContextEngineID(profile.engineId);
        } else {
            // PDU class is for SNMPv2c units
            pdu = new PDU();
        }
        pdu.setType(PDU.TRAP);

        addPayload(pdu, event, profile);

        Target target = createTarget(profile);
        for (Host host : profile.hosts) {
            try {
                log.info("Generate an snmp trap for event: {} to address: {} ",
                    event, host.name);
                target.setAddress(
                    new UdpAddress(
                        InetAddress.getByName(host.name),
                        host.port
                    )
                );
                snmpForProfile.send(pdu, target);
                notifyObservers(DispatchResult.success(event, address, EventNotificationMethod.SNMP));
            } catch (Exception e) {
                log.error(e.getMessage());
                notifyObservers(DispatchResult.failure(event, address, EventNotificationMethod.SNMP, e.getMessage()));
            }
        }
    }

    private synchronized org.snmp4j.Snmp getSnmpForProfile(Profile profile) {
        if (!profileSnmpMap.containsKey(profile)) {
            profileSnmpMap.put(profile,
                    profile.version == SnmpConstants.version3 ? createSnmp3(profile) : createSnmp());
        }
        return profileSnmpMap.get(profile);
    }

    private org.snmp4j.Snmp createSnmp() {
        try {
            // Create a new session and define it's transport.
            return new org.snmp4j.Snmp(new DefaultUdpTransportMapping());
        } catch (IOException e) {
            throw new NotificationServiceException("error creating " + getClass().getName());
        }
    }

    private org.snmp4j.Snmp createSnmp3(Profile profile) {
        try {
            TransportMapping<?> transport = new DefaultUdpTransportMapping();
            org.snmp4j.Snmp snmp = new org.snmp4j.Snmp(transport);
            SecurityProtocols securityProtocols = SecurityProtocols.getInstance();
            securityProtocols.addDefaultProtocols();
            securityProtocols.addAuthenticationProtocol(new AuthMD5());
            securityProtocols.addAuthenticationProtocol(new AuthSHA());
            securityProtocols.addPrivacyProtocol(new PrivAES128());
            securityProtocols.addPrivacyProtocol(new PrivAES192());
            securityProtocols.addPrivacyProtocol(new PrivAES256());
            USM usm = new USM(securityProtocols, profile.engineId, 0);
            ((org.snmp4j.mp.MPv3) snmp.getMessageProcessingModel(org.snmp4j.mp.MPv3.ID))
                    .setLocalEngineID(profile.engineId.getValue());
            ((org.snmp4j.mp.MPv3) snmp.getMessageProcessingModel(org.snmp4j.mp.MPv3.ID))
                    .getSecurityModels().addSecurityModel(usm);
            SecurityModels.getInstance().addSecurityModel(
                    usm);
            transport.listen();
            snmp.getUSM().addUser(
                    profile.username,
                    getUsmUser(profile));
            return snmp;
        } catch (IOException e) {
            throw new NotificationServiceException("error creating version 3 snmp " + getClass().getName());
        }
    }

    private UsmUser getUsmUser(Profile profile) {
        OID authenticationProtocol = null;
        OctetString authenticationPassphrase = null;
        OID privacyProtocol = null;
        OctetString privacyPassphrase = null;
        switch(profile.securityLevel) {
            case SecurityLevel.AUTH_NOPRIV:
                authenticationProtocol = profile.authProtocol;
                authenticationPassphrase = profile.authPassphrase;
                break;
            case SecurityLevel.AUTH_PRIV:
                authenticationProtocol = profile.authProtocol;
                authenticationPassphrase = profile.authPassphrase;
                privacyProtocol = profile.privProtocol;
                privacyPassphrase = profile.privacyPassphrase;
                break;
        }
        return new UsmUser(profile.username,
                authenticationProtocol,
                authenticationPassphrase,
                privacyProtocol,
                privacyPassphrase);
    }

    private Target createTarget(Profile profile) {
        Target target;
        if (profile.version == SnmpConstants.version2c) {
            target = new CommunityTarget();
            ((CommunityTarget) target).setCommunity(profile.community);
        } else {
            target = new UserTarget();
            ((UserTarget) target).setAuthoritativeEngineID(profile.engineId.getValue());
            target.setSecurityName(profile.username);
            target.setSecurityLevel(profile.securityLevel);
        }
        target.setVersion(profile.version);
        return target;
    }

    private void addPayload(PDU pdu, AuditLogEvent event, Profile profile) {

        pdu.add(new VariableBinding(SnmpConstants.sysUpTime,
                                      new TimeTicks((System.nanoTime() - nanoStart) / 10000000)));
        // { [baseoid] notifications(0) audit(1) }
        pdu.add(new VariableBinding(SnmpConstants.snmpTrapOID,
                                      SnmpConstants.getTrapOID(new OID(profile.oid),
                                                               ENTERPRISE_SPECIFIC,
                                                               AUDIT)));

        int auditLogId = AuditLogType.UNASSIGNED.getValue();
        try {
            auditLogId = AuditLogType.valueOf(event.getName()).getValue();
        } catch (IllegalArgumentException e) {
            log.warn("Could not find event: {} in auditLogTypes", event.getName());
        }
        // { [baseoid] objects(2) audit(1) }
        OID auditObjects = new OID(profile.oid).append(OBJECTS_AUDIT);

        addString(pdu, auditObjects, INSTANCE_ID, Long.toString(event.getId()), true);
        addString(pdu, auditObjects, NAME, event.getName(), true);
        addInt(pdu, auditObjects, ID, auditLogId, true);
        addInt(pdu, auditObjects, SEVERITY, event.getSeverity().getValue(), true);
        addString(pdu, auditObjects, MESSAGE, event.getMessage(), true);
        addInt(pdu, auditObjects, STATUS, event.getType().getValue(), true);
        addString(pdu, auditObjects, DATETIME, new SimpleDateFormat(ISO8601).format(event.getLogTime()), true);

        // Optional pdu:
        addString(pdu, auditObjects, USERNAME, event.getUserName(), false);
        addUuid(pdu, auditObjects, USER_ID, event.getUserId());
        addString(pdu, auditObjects, VM_NAME, event.getVmName(), false);
        addUuid(pdu, auditObjects, VM_ID, event.getVmId());
        addString(pdu, auditObjects, VDS_NAME, event.getVdsName(), false);
        addUuid(pdu, auditObjects, VDS_ID, event.getVdsId());
        addString(pdu, auditObjects, VM_TEMPLATE_NAME, event.getVmTemplateName(), false);
        addUuid(pdu, auditObjects, VM_TEMPLATE_ID, event.getVmTemplateId());
        addString(pdu, auditObjects, STORAGE_POOL_NAME, event.getStoragePoolName(), false);
        addUuid(pdu, auditObjects, STORAGE_POOL_ID, event.getStoragePoolId());
        addString(pdu, auditObjects, STORAGE_DOMAIN_NAME, event.getStorageDomainName(), false);
        addUuid(pdu, auditObjects, STORAGE_DOMAIN_ID, event.getStorageDomainId());
    }

    private void addString(PDU pdu, OID prefix, int suffix, String val, boolean allowEmpty) {
        if (allowEmpty || !StringUtils.isEmpty(val)) {
            pdu.add(new VariableBinding(new OID(prefix).append(suffix), new OctetString(val == null ? "" : val)));
        }
    }

    private void addInt(PDU pdu, OID prefix, int suffix, Integer val, boolean allowEmpty) {
        if (allowEmpty || val != null) {
            pdu.add(new VariableBinding(new OID(prefix).append(suffix), new Integer32(val == null ? 0 : val)));
        }
    }

    private void addUuid(PDU pdu, final OID prefix, int suffix, Guid val) {
        if (!Guid.isNullOrEmpty(val)) {
            addString(pdu, prefix, suffix, val.toString(), false);
        }
    }

    static class Host {
        public String name;
        public int port = 162;

        public Host(String name, String port) {
            this.name = name;
            if (port != null) {
                try {
                    this.port = Integer.parseInt(port);
                    if (this.port <= 0 || this.port > 0xffff) {
                        throw new Exception("Bad port");
                    }
                } catch (Exception e) {
                    throw new IllegalArgumentException(
                        String.format(
                            "Invalid port %s for snmp host %s",
                            port,
                            name
                        )
                    );
                }
            }
        }
    }

    static class Profile {
        private static final Pattern HOST_PATTERN = Pattern.compile(
            "(?<host>(([^\\[:\\s]+)|(\\[[^\\]]+\\])))(:(?<port>[^\\s]*))?");

        public List<Host> hosts = new LinkedList<>();
        public int version;
        public OID oid;

        // Version 2 specific variables
        public OctetString community;

        // Version 3 specific variables
        public OctetString engineId;
        public OctetString username;
        public OID authProtocol;
        public OctetString authPassphrase;
        public OID privProtocol;
        public OctetString privacyPassphrase;
        public int securityLevel;

        public Profile(String managers, String oid, int version) {
            Matcher m = HOST_PATTERN.matcher(managers);
            while (m.find()) {
                hosts.add(new Host(m.group("host"), m.group("port")));
            }
            this.oid = new OID(oid);
            if (!this.oid.isValid()) {
                throw new IllegalArgumentException(
                        String.format(
                                "Invalid oid '%s'",
                                oid
                        )
                );

            }
            this.version = version;
        }

        public static Profile buildProfile(String managers, String community, String oid, int version) {
            Profile profile = new Profile(managers, oid, version);
            profile.community = new OctetString(community);
            return profile;
        }

        public static Profile buildProfile(String managers,
                                           String engineId,
                                           String username,
                                           OID authProtocol,
                                           String authPassphrase,
                                           OID privProtocol,
                                           String privacyPassphrase,
                                           int securityLevel,
                                           String oid,
                                           int version) {
            Profile profile = new Profile(managers, oid, version);
            profile.engineId = OctetString.fromHexString(engineId);
            profile.username = new OctetString(username);
            profile.authProtocol = authProtocol;
            profile.authPassphrase = new OctetString(authPassphrase);
            profile.privProtocol = privProtocol;
            profile.privacyPassphrase = new OctetString(privacyPassphrase);
            profile.securityLevel = securityLevel;
            return profile;
        }
    }
}
