/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.cloudstack.storage.datastore.util;

import java.util.HashMap;
import java.util.LinkedList;

import org.apache.cloudstack.storage.datastore.util.NexentaNmsClient.NmsResponse;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import com.cloud.utils.exception.CloudRuntimeException;
import com.google.gson.annotations.SerializedName;

public class NexentaStorAppliance {
    private static final Logger logger = LogManager.getLogger(NexentaStorAppliance.class);

    protected NexentaNmsClient client;
    protected NexentaUtil.NexentaPluginParameters parameters;

    public NexentaStorAppliance(NexentaUtil.NexentaPluginParameters parameters) {
        client = new NexentaNmsClient(parameters.getNmsUrl());
        this.parameters = parameters;
    }

    NexentaStorAppliance(NexentaNmsClient client, NexentaUtil.NexentaPluginParameters parameters) {
        this.client = client;
        this.parameters = parameters;
    }

    String getVolumeName(String volumeName) {
        if (volumeName.startsWith("/")) {
            return String.format("%s%s", parameters.getVolume(), volumeName);
        }
        return String.format("%s/%s", parameters.getVolume(), volumeName);
    }

    static String getTargetName(String volumeName) {
        return NexentaUtil.ISCSI_TARGET_NAME_PREFIX + volumeName;
    }

    static String getTargetGroupName(String volumeName) {
        return NexentaUtil.ISCSI_TARGET_GROUP_PREFIX + volumeName;
    }

    @SuppressWarnings("unused")
    static final class IntegerNmsResponse extends NmsResponse {
        Integer result;

        IntegerNmsResponse(int result) {
            this.result = Integer.valueOf(result);
        }

        public Integer getResult() {
            return result;
        }
    }

    @SuppressWarnings("unused")
    static final class IscsiTarget {
        protected String status;
        protected String protocol;
        protected String name;
        protected String sessions;
        protected String alias;
        protected String provider;

        IscsiTarget(String status, String protocol, String name, String sessions, String alias, String provider) {
            this.status = status;
            this.protocol = protocol;
            this.name = name;
            this.sessions = sessions;
            this.alias = alias;
            this.provider = provider;
        }
    }

    @SuppressWarnings("unused")
    static final class ListOfIscsiTargetsNmsResponse extends NmsResponse {
        protected HashMap<String, IscsiTarget> result;

        ListOfIscsiTargetsNmsResponse() {}

        ListOfIscsiTargetsNmsResponse(HashMap<String, IscsiTarget> result) {
            this.result = result;
        }

        public HashMap<String, IscsiTarget> getResult() {
            return result;
        }
    }

    /**
     * Checks if iSCSI target exists.
     * @param targetName iSCSI target name
     * @return true if iSCSI target exists, else false
     */
    boolean isIscsiTargetExists(String targetName) {
        ListOfIscsiTargetsNmsResponse response = (ListOfIscsiTargetsNmsResponse) client.execute(ListOfIscsiTargetsNmsResponse.class, "stmf", "list_targets");
        if (response == null) {
            return false;
        }
        HashMap<String, IscsiTarget> result = response.getResult();
        return result != null && result.keySet().contains(targetName);
    }

    @SuppressWarnings("unused")
    static final class CreateIscsiTargetRequestParams {
        @SerializedName("target_name") String targetName;

        CreateIscsiTargetRequestParams(String targetName) {
            this.targetName = targetName;
        }

        @Override
        public boolean equals(Object other) {
            return other instanceof CreateIscsiTargetRequestParams && targetName.equals(((CreateIscsiTargetRequestParams) other).targetName);
        }
    }

    /**
     * Creates iSCSI target on NexentaStor Appliance.
     * @param targetName iSCSI target name
     */
    void createIscsiTarget(String targetName) {
        try {
            client.execute(NmsResponse.class, "iscsitarget", "create_target", new CreateIscsiTargetRequestParams(targetName));
        } catch (CloudRuntimeException ex) {
            if (!ex.getMessage().contains("already configured")) {
                throw ex;
            }
            logger.debug("Ignored target creation error: " + ex);
        }
    }

    @SuppressWarnings("unused")
    static final class ListOfStringsNmsResponse extends NmsResponse {
        LinkedList<String> result;

        ListOfStringsNmsResponse() {}

        ListOfStringsNmsResponse(LinkedList<String> result) {
            this.result = result;
        }

        public LinkedList<String> getResult() {
            return result;
        }
    }

    /**
     * Checks if iSCSI target group already exists on NexentaStor Appliance.
     * @param targetGroupName iSCSI target group name
     * @return true if iSCSI target group already exists, else false
     */
    boolean isIscsiTargetGroupExists(String targetGroupName) {
        ListOfStringsNmsResponse response = (ListOfStringsNmsResponse) client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroups");
        if (response == null) {
            return false;
        }
        LinkedList<String> result = response.getResult();
        return result != null && result.contains(targetGroupName);
    }

    /**
     * Creates iSCSI target group on NexentaStor Appliance.
     * @param targetGroupName iSCSI target group name
     */
    void createIscsiTargetGroup(String targetGroupName) {
        try {
            client.execute(NmsResponse.class, "stmf", "create_targetgroup", targetGroupName);
        } catch (CloudRuntimeException ex) {
            if (!ex.getMessage().contains("already exists") && !ex.getMessage().contains("target must be offline")) {
                throw ex;
            }
            logger.info("Ignored target group creation error: " + ex);
        }
    }

    /**
     * Checks if iSCSI target is member of target group.
     * @param targetGroupName iSCSI target group name
     * @param targetName iSCSI target name
     * @return true if target is member of iSCSI target group, else false
     */
    boolean isTargetMemberOfTargetGroup(String targetGroupName, String targetName) {
        ListOfStringsNmsResponse response = (ListOfStringsNmsResponse) client.execute(ListOfStringsNmsResponse.class, "stmf", "list_targetgroup_members", targetGroupName);
        if (response == null) {
            return false;
        }
        LinkedList<String> result = response.getResult();
        return result != null && result.contains(targetName);
    }

    /**
     * Adds iSCSI target to target group.
     * @param targetGroupName iSCSI target group name
     * @param targetName iSCSI target name
     */
    void addTargetGroupMember(String targetGroupName, String targetName) {
        try {
            client.execute(NmsResponse.class, "stmf", "add_targetgroup_member", targetGroupName, targetName);
        } catch (CloudRuntimeException ex) {
            if (!ex.getMessage().contains("already exists") && !ex.getMessage().contains("target must be offline")) {
                throw ex;
            }
            logger.debug("Ignored target group member addition error: " + ex);
        }
    }

    /**
     * Checks if LU already exists on NexentaStor appliance.
     * @param luName LU name
     * @return true if LU already exists, else false
     */
    boolean isLuExists(String luName) {
        IntegerNmsResponse response;
        try {
            response = (IntegerNmsResponse) client.execute(IntegerNmsResponse.class, "scsidisk", "lu_exists", luName);
        } catch (CloudRuntimeException ex) {
            if (ex.getMessage().contains("does not exist")) {
                return false;
            }
            throw ex;
        }
        return response!= null && response.getResult() > 0;
    }

    @SuppressWarnings("unused")
    static final class LuParams {
        @Override
        public boolean equals(Object other) {
            return other instanceof LuParams;
        }
    }

    /**
     * Creates LU for volume.
     * @param volumeName volume name
     */
    void createLu(String volumeName) {
        try {
            client.execute(NmsResponse.class, "scsidisk", "create_lu", volumeName, new LuParams());
        } catch (CloudRuntimeException ex) {
            if (!ex.getMessage().contains("in use")) {
                throw ex;
            }
            logger.info("Ignored LU creation error: " + ex);
        }
    }

    /**
     * Checks if LU shared on NexentaStor appliance.
     * @param luName LU name
     * @return true if LU was already shared, else false
     */
    boolean isLuShared(String luName) {
        IntegerNmsResponse response;
        try {
            response = (IntegerNmsResponse) client.execute(IntegerNmsResponse.class, "scsidisk", "lu_shared", luName);
        } catch (CloudRuntimeException ex) {
            if (ex.getMessage().contains("does not exist")) {
                return false;
            }
            throw ex;
        }
        return response != null && response.getResult() > 0;
    }

    @SuppressWarnings("unused")
    static final class MappingEntry {
        @SerializedName("target_group") String targetGroup;
        String lun;
        String zvol;
        @SerializedName("host_group") String hostGroup;
        @SerializedName("entry_number") String entryNumber;

        MappingEntry(String targetGroup, String lun) {
            this.targetGroup = targetGroup;
            this.lun = lun;
        }

        static boolean isEquals(Object a, Object b) {
            return (a == null && b == null) || (a != null && a.equals(b));
        }

        @Override
        public boolean equals(Object other) {
            if (other instanceof MappingEntry) {
                MappingEntry o = (MappingEntry) other;
                return isEquals(targetGroup, o.targetGroup) && isEquals(lun, o.lun) && isEquals(zvol, o.zvol) &&
                        isEquals(hostGroup, o.hostGroup) && isEquals(entryNumber, o.entryNumber);
            }
            return false;
        }
    }

    @SuppressWarnings("unused")
    static final class AddMappingEntryNmsResponse extends NmsResponse {
        MappingEntry result;
    }

    /**
     * Adds LU mapping entry to iSCSI target group.
     * @param luName LU name
     * @param targetGroupName iSCSI target group name
     */
    void addLuMappingEntry(String luName, String targetGroupName) {
        MappingEntry mappingEntry = new MappingEntry(targetGroupName, "0");
        try {
            client.execute(AddMappingEntryNmsResponse.class, "scsidisk", "add_lun_mapping_entry", luName, mappingEntry);
        } catch (CloudRuntimeException ex) {
            if (!ex.getMessage().contains("view already exists")) {
                throw ex;
            }
            logger.debug("Ignored LU mapping entry addition error " + ex);
        }
    }

    NexentaStorZvol createIscsiVolume(String volumeName, Long volumeSize) {
        final String zvolName = getVolumeName(volumeName);
        String volumeSizeString = String.format("%dB", volumeSize);

        client.execute(NmsResponse.class, "zvol", "create", zvolName, volumeSizeString, parameters.getVolumeBlockSize(), parameters.getSparseVolumes());

        final String targetName = getTargetName(volumeName);
        final String targetGroupName = getTargetGroupName(volumeName);

        if (!isIscsiTargetExists(targetName)) {
            createIscsiTarget(targetName);
        }

        if (!isIscsiTargetGroupExists(targetGroupName)) {
            createIscsiTargetGroup(targetGroupName);
        }

        if (!isTargetMemberOfTargetGroup(targetGroupName, targetName)) {
            addTargetGroupMember(targetGroupName, targetName);
        }

        if (!isLuExists(zvolName)) {
            createLu(zvolName);
        }

        if (!isLuShared(zvolName)) {
            addLuMappingEntry(zvolName, targetGroupName);
        }

        return new NexentaStorZvol(zvolName, targetName);
    }

    static abstract class NexentaStorVolume {
        protected String name;

        NexentaStorVolume(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return name;
        }
    }

    public static final class NexentaStorZvol extends NexentaStorVolume {
        protected String iqn;

        public NexentaStorZvol(String name, String iqn) {
            super(name);
            this.iqn = iqn;
        }

        public String getIqn() {
            return iqn;
        }
    }

    public void deleteIscsiVolume(String volumeName) {
        try {
            NmsResponse response = client.execute(NmsResponse.class, "zvol", "destroy", volumeName, "");
        } catch (CloudRuntimeException ex) {
            if (!ex.getMessage().contains("does not exist")) {
                throw ex;
            }
            logger.debug(String.format(
                    "Volume %s does not exist, it seems it was already " +
                            "deleted.", volumeName));
        }
    }

    public NexentaStorVolume createVolume(String volumeName, Long volumeSize) {
        return createIscsiVolume(volumeName, volumeSize);
    }

    public void deleteVolume(String volumeName) {
        deleteIscsiVolume(volumeName);
    }
}
