/*
 *  Copyright (c) 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. 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.wso2.carbon.analytics.datasource.rdbms;


import com.google.common.collect.Maps;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.analytics.data.commons.exception.AnalyticsException;
import org.wso2.carbon.analytics.data.commons.service.AnalyticsDataHolder;
import org.wso2.carbon.analytics.data.commons.utils.AnalyticsCommonUtils;

import java.io.File;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.sql.DataSource;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

/**
 * Utility methods for RDBMS based operations for analytics data source.
 */
public class RDBMSUtils {

    private static final String VERSION = "Version";
    public static final String ANALYTICS_CONF_DIR = "analytics";
    private static final String DATABASE_PRODUCT_NAME = "Database Product Name";
    private static final Log log = LogFactory.getLog(RDBMSUtils.class);
    private static final String RDBMS_QUERY_CONFIG_FILE = "rdbms-config.xml";
    private static RDBMSConfigurationMapper mapper;

    public static Map<String, Object> lookupDatabaseInfo(DataSource ds) throws AnalyticsException {
        Connection conn = null;
        try {
            conn = ds.getConnection();
            DatabaseMetaData dmd = conn.getMetaData();
            Map<String, Object> result = new HashMap<>();
            result.put(DATABASE_PRODUCT_NAME, dmd.getDatabaseProductName());
            result.put(VERSION, Double.parseDouble(dmd.getDatabaseMajorVersion() + "."
                                                   + dmd.getDatabaseMinorVersion()));
            return result;
        } catch (SQLException e) {
            throw new AnalyticsException("Error in looking up database type: " + e.getMessage(), e);
        } finally {
            RDBMSUtils.cleanupConnection(null, null, conn);
        }
    }

    private static RDBMSConfigurationMapper loadRDBMSConfigurationMapper() throws AnalyticsException {
        if (mapper == null) {
            RDBMSQueryConfiguration config = loadQueryConfiguration();
            mapper = new RDBMSConfigurationMapper(config);
        }
        return mapper;
    }

    public static RDBMSQueryConfigurationEntry lookupCurrentQueryConfigurationEntry(
            DataSource ds, String category) throws AnalyticsException {
        Map<String, Object> dbInfo = lookupDatabaseInfo(ds);
        RDBMSConfigurationMapper mapper = loadRDBMSConfigurationMapper();
        RDBMSQueryConfigurationEntry entry = mapper.lookupEntry((String) dbInfo.get(DATABASE_PRODUCT_NAME),
                                                                (double) dbInfo.get(VERSION), category);
        if (entry != null) {
            return entry;
        } else {
            dbInfo.put("Category", category);
            throw new AnalyticsException("Cannot find a database section in the RDBMS "
                                         + "configuration for the database: " + dbInfo);
        }
    }

    public static RDBMSQueryConfiguration loadQueryConfiguration() throws AnalyticsException {
        // TODO: Finalize logic for RDBMS config files
        File confFile = AnalyticsCommonUtils.loadConfigFile(ANALYTICS_CONF_DIR, RDBMS_QUERY_CONFIG_FILE);

        try {
            if (confFile == null || !confFile.exists()) {
                throw new AnalyticsException("Cannot find RDBMS query configuration file: " + RDBMS_QUERY_CONFIG_FILE);
            }
            JAXBContext ctx = JAXBContext.newInstance(RDBMSQueryConfiguration.class);
            Unmarshaller unmarshaller = ctx.createUnmarshaller();
            RDBMSQueryConfiguration conf = (RDBMSQueryConfiguration) unmarshaller.unmarshal(confFile);
            validateRDBMSQueryConfiguration(conf);
            return conf;
        } catch (JAXBException e) {
            throw new AnalyticsException(
                    "Error in processing RDBMS query configuration: " + e.getMessage(), e);
        }
    }

    private static void validateRDBMSQueryConfiguration(RDBMSQueryConfiguration conf) throws AnalyticsException {
        for (RDBMSQueryConfigurationEntry entry : conf.getDatabases()) {
            if (isEmpty(entry.getRecordMergeQuery()) && (isEmpty(entry.getRecordInsertQuery()) ||
                                                         isEmpty(entry.getRecordUpdateQuery()))) {
                throw new AnalyticsException("RDBMS configuration database entry: " +
                                             entry.getDatabaseName()
                                             + ", either record merge query or both insert/update " +
                                             "queries must be provided");
            }
        }
    }

    private static boolean isEmpty(String field) {
        return (field == null || field.trim().length() == 0);
    }

    public static void cleanupConnection(ResultSet rs, Statement stmt, Connection conn) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException ignore) { /* ignore */ }
        }
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException ignore) { /* ignore */ }
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException ignore) { /* ignore */ }
        }
    }

    public static void rollbackConnection(Connection conn) {
        if (conn != null) {
            try {
                conn.rollback();
            } catch (SQLException ignore) { /* ignore */ }
        }
    }

    public static void executeAllUpdateQueries(Connection conn, Map<String, Object[]> queries)
            throws AnalyticsException {
        StringBuilder messages = new StringBuilder();
        PreparedStatement stmt = null;
        for (Map.Entry<String, Object[]> entry : queries.entrySet()) {
            try {
                stmt = conn.prepareStatement(entry.getKey());
                for (int i = 0; i < entry.getValue().length; i++) {
                    stmt.setObject(i + 1, entry.getValue()[i]);
                }
                stmt.execute();
            } catch (SQLException e) {
                messages.append(e.getMessage()).append("\n");
            } finally {
                RDBMSUtils.cleanupConnection(null, stmt, null);
            }
        }
        String exs = messages.toString();
        if (exs.length() > 0) {
            if (log.isDebugEnabled()) {
                log.debug("executeAllUpdateQueries exceptions: [" + exs + "]");
            }
            throw new AnalyticsException("Error in executing SQL queries: " + exs);
        }
    }

    public static Map<String, Object[]> generateNoParamQueryMap(String[] queries) {
        Map<String, Object[]> result = new LinkedHashMap<>(queries.length);
        for (String query : queries) {
            result.put(query, new Object[0]);
        }
        return result;
    }

    /**
     * RDBMS configuration mapping class to be used to lookup matching configuration entry with a data source.
     */
    private static class RDBMSConfigurationMapper {

        private List<Map.Entry<Pattern, RDBMSQueryConfigurationEntry>> entries =
                new ArrayList<>();

        public RDBMSConfigurationMapper(RDBMSQueryConfiguration config) {
            for (RDBMSQueryConfigurationEntry entry : config.getDatabases()) {
                this.entries.add(Maps.immutableEntry(Pattern.compile(entry.getDatabaseName().toLowerCase()), entry));
            }
        }

        private boolean checkVersion(RDBMSQueryConfigurationEntry entry, double version) {
            double minVersion = entry.getMinVersion();
            double maxVersion = entry.getMaxVersion();
            return !(minVersion != 0 && version < minVersion) && !(maxVersion != 0 && version > maxVersion);
        }

        private List<RDBMSQueryConfigurationEntry> extractMatchingConfigEntries(String dbName) {
            return this.entries.stream().filter(entry -> entry.getKey().matcher(dbName).find())
                    .map(Map.Entry::getValue).collect(Collectors.toList());
        }

        public RDBMSQueryConfigurationEntry lookupEntry(String dbName, double version, String category) {
            List<RDBMSQueryConfigurationEntry> dbResults = this.extractMatchingConfigEntries(dbName.toLowerCase());
            if (dbResults == null || dbResults.isEmpty()) {
                return null;
            }
            List<RDBMSQueryConfigurationEntry> versionResults = new ArrayList<>();
            for (RDBMSQueryConfigurationEntry entry : dbResults) {
                if (this.checkVersion(entry, version)) {
                    versionResults.add(entry);
                }
            }
            if (versionResults.isEmpty()) {
                return null;
            }
            if (category == null) {
                return versionResults.get(0);
            } else {
                RDBMSQueryConfigurationEntry defaultResult = null;
                /* to put the top most empty category entry as the default one */
                boolean defaultSet = false;
                for (RDBMSQueryConfigurationEntry entry : versionResults) {
                    if (category.equalsIgnoreCase(entry.getCategory())) {
                        return entry;
                    } else if (!defaultSet && entry.getCategory() == null) {
                        defaultResult = entry;
                        defaultSet = true;
                    }
                }
                return defaultResult;
            }
        }

    }

}
