// 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
// 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 com.cloud.utils.db;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.PrimaryKeyJoinColumns;
import javax.persistence.SecondaryTable;
import javax.persistence.SecondaryTables;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import static com.cloud.utils.AutoCloseableUtil.closeAutoCloseable;

public class DbUtil {
    protected static Logger LOGGER = LogManager.getLogger(DbUtil.class);

    private static Map<String, Connection> s_connectionForGlobalLocks = new HashMap<String, Connection>();

    public static Connection getConnectionForGlobalLocks(String name, boolean forLock) {
        synchronized (s_connectionForGlobalLocks) {
            if (forLock) {
                if (s_connectionForGlobalLocks.get(name) != null) {
                    LOGGER.error("Sanity check failed, global lock name " + name + " is already in use");
                    assert (false);
                }

                Connection connection = TransactionLegacy.getStandaloneConnection();
                if (connection != null) {
                    try {
                        connection.setAutoCommit(true);
                    } catch (SQLException e) {
                        closeAutoCloseable(connection, "error closing connection for global locks");
                        return null;
                    }
                    s_connectionForGlobalLocks.put(name, connection);
                    return connection;
                }
                return null;
            } else {
                Connection connection = s_connectionForGlobalLocks.get(name);
                s_connectionForGlobalLocks.remove(name);
                return connection;
            }
        }
    }

    public static void removeConnectionForGlobalLocks(String name) {
        synchronized (s_connectionForGlobalLocks) {
            s_connectionForGlobalLocks.remove(name);
        }
    }

    public static String getColumnName(Field field, AttributeOverride[] overrides) {
        if (overrides != null) {
            for (AttributeOverride override : overrides) {
                if (override.name().equals(field.getName())) {
                    return override.column().name();
                }
            }
        }

        assert (field.getAnnotation(Embedded.class) == null) : "Cannot get column name from embedded field: " + field.getName();

        Column column = field.getAnnotation(Column.class);
        return column != null ? column.name() : field.getName();
    }

    public static String getColumnName(Field field) {
        return getColumnName(field, null);
    }

    public static String getReferenceColumn(PrimaryKeyJoinColumn pkjc) {
        return pkjc.referencedColumnName().length() != 0 ? pkjc.referencedColumnName() : pkjc.name();
    }

    public static PrimaryKeyJoinColumn[] getPrimaryKeyJoinColumns(Class<?> clazz) {
        PrimaryKeyJoinColumn pkjc = clazz.getAnnotation(PrimaryKeyJoinColumn.class);
        if (pkjc != null) {
            return new PrimaryKeyJoinColumn[] {pkjc};
        }

        PrimaryKeyJoinColumns pkjcs = clazz.getAnnotation(PrimaryKeyJoinColumns.class);
        if (pkjcs != null) {
            return pkjcs.value();
        }

        return null;
    }

    public static Field findField(Class<?> clazz, String columnName) {
        for (Field field : clazz.getDeclaredFields()) {
            if (field.getAnnotation(Embedded.class) != null || field.getAnnotation(EmbeddedId.class) != null) {
                findField(field.getType(), columnName);
            } else {
                if (columnName.equals(DbUtil.getColumnName(field))) {
                    return field;
                }
            }
        }
        return null;
    }

    public static final AttributeOverride[] getAttributeOverrides(AnnotatedElement ae) {
        AttributeOverride[] overrides = null;

        AttributeOverrides aos = ae.getAnnotation(AttributeOverrides.class);
        if (aos != null) {
            overrides = aos.value();
        }

        if (overrides == null || overrides.length == 0) {
            AttributeOverride override = ae.getAnnotation(AttributeOverride.class);
            if (override != null) {
                overrides = new AttributeOverride[1];
                overrides[0] = override;
            } else {
                overrides = new AttributeOverride[0];
            }
        }

        return overrides;
    }

    public static final boolean isPersistable(Field field) {
        if (field.getAnnotation(Transient.class) != null) {
            return false;
        }

        int modifiers = field.getModifiers();
        return !(Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers));
    }

    public static final boolean isIdField(Field field) {
        if (field.getAnnotation(Id.class) != null) {
            return true;
        }

        if (field.getAnnotation(EmbeddedId.class) != null) {
            assert (field.getType().getAnnotation(Embeddable.class) != null) : "Class " + field.getType().getName() + " must be Embeddable to be used as Embedded Id";
            return true;
        }

        return false;
    }

    public static final SecondaryTable[] getSecondaryTables(AnnotatedElement clazz) {
        SecondaryTable[] sts = null;
        SecondaryTable stAnnotation = clazz.getAnnotation(SecondaryTable.class);
        if (stAnnotation == null) {
            SecondaryTables stsAnnotation = clazz.getAnnotation(SecondaryTables.class);
            sts = stsAnnotation != null ? stsAnnotation.value() : new SecondaryTable[0];
        } else {
            sts = new SecondaryTable[] {stAnnotation};
        }

        return sts;
    }

    public static final String getTableName(Class<?> clazz) {
        Table table = clazz.getAnnotation(Table.class);
        return table != null ? table.name() : clazz.getSimpleName();
    }

    public static boolean getGlobalLock(String name, int timeoutSeconds) {
        Connection conn = getConnectionForGlobalLocks(name, true);
        if (conn == null) {
            LOGGER.error("Unable to acquire DB connection for global lock system");
            return false;
        }

        try (PreparedStatement pstmt = conn.prepareStatement("SELECT COALESCE(GET_LOCK(?, ?),0)");) {
            pstmt.setString(1, name);
            pstmt.setInt(2, timeoutSeconds);

            try (ResultSet rs = pstmt.executeQuery();) {
                if (rs != null && rs.first()) {
                    if (rs.getInt(1) > 0) {
                        return true;
                    } else {
                        if (LOGGER.isDebugEnabled())
                            LOGGER.debug("GET_LOCK() timed out on lock : " + name);
                    }
                }
            }
        } catch (SQLException e) {
            LOGGER.error("GET_LOCK() throws exception ", e);
        } catch (Throwable e) {
            LOGGER.error("GET_LOCK() throws exception ", e);
        }

        removeConnectionForGlobalLocks(name);
        closeAutoCloseable(conn, "connection for global lock");
        return false;
    }

    public static Class<?> getEntityBeanType(GenericDao<?, Long> dao) {
        return dao.getEntityBeanType();
    }

    public static boolean releaseGlobalLock(String name) {
        try (Connection conn = getConnectionForGlobalLocks(name, false);) {
            if (conn == null) {
                LOGGER.error("Unable to acquire DB connection for global lock system");
                assert (false);
                return false;
            }

            try (PreparedStatement pstmt = conn.prepareStatement("SELECT COALESCE(RELEASE_LOCK(?), 0)");) {
                pstmt.setString(1, name);
                try (ResultSet rs = pstmt.executeQuery();) {
                    if (rs != null && rs.first()) {
                        return rs.getInt(1) > 0;
                    }
                    LOGGER.error("releaseGlobalLock:RELEASE_LOCK() returns unexpected result");
                }
            }
        } catch (SQLException e) {
            LOGGER.error("RELEASE_LOCK() throws exception ", e);
        } catch (Throwable e) {
            LOGGER.error("RELEASE_LOCK() throws exception ", e);
        }
        return false;
    }

    public static void closeResources(final Connection connection, final Statement statement, final ResultSet resultSet) {

        closeResultSet(resultSet);
        closeStatement(statement);
        closeConnection(connection);

    }

    public static void closeResources(final Statement statement, final ResultSet resultSet) {

        closeResources(null, statement, resultSet);

    }

    public static void closeResultSet(final ResultSet resultSet) {
        closeAutoCloseable(resultSet, "exception while closing result set.");
    }

    public static void closeStatement(final Statement statement) {
        closeAutoCloseable(statement, "exception while closing statement.");
    }

    public static void closeConnection(final Connection connection) {
        closeAutoCloseable(connection, "exception while close connection.");
    }

    public static Map<String, String> getDbInfo(String type, String ... var) {
        String vars = String.join(",", var);
        Map<String, String> result = new HashMap<>();
        String sql = String.format("SHOW %s WHERE FIND_IN_SET(Variable_name,?)",type);
        try (TransactionLegacy txn = TransactionLegacy.open("metrics")) {
            PreparedStatement pstmt = txn.prepareAutoCloseStatement(sql);
            pstmt.setString(1, vars);
            final ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                String variableName = rs.getString("Variable_name");
                String value = rs.getString("value");
                result.put(variableName, value);
            }
        } catch (SQLException e) {
            LOGGER.error("failed to get the database status: " + e.getLocalizedMessage());
            LOGGER.debug("failed to get the database status", e);
        }
        return result;
    }

}
