/*
 * 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.kylin.rest.service;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.kylin.cube.CubeInstance;
import org.apache.kylin.metadata.project.ProjectInstance;
import org.apache.kylin.metadata.project.RealizationEntry;
import org.apache.kylin.metadata.realization.RealizationType;
import org.apache.kylin.metrics.MetricsManager;
import org.apache.kylin.metrics.lib.impl.TimePropertyEnum;
import org.apache.kylin.metrics.property.JobPropertyEnum;
import org.apache.kylin.metrics.property.QueryPropertyEnum;
import org.apache.kylin.rest.constant.Constant;
import org.apache.kylin.rest.exception.BadRequestException;
import org.apache.kylin.rest.request.PrepareSqlRequest;
import org.apache.kylin.rest.response.MetricsResponse;
import org.apache.kylin.rest.response.SQLResponse;
import org.apache.kylin.storage.hybrid.HybridInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;

import org.apache.kylin.shaded.com.google.common.base.Strings;
import org.apache.kylin.shaded.com.google.common.collect.Lists;

@Component("dashboardService")
public class DashboardService extends BasicService {

    private static final Logger logger = LoggerFactory.getLogger(DashboardService.class);

    @Autowired
    private CubeService cubeService;

    public MetricsResponse getCubeMetrics(String projectName, String cubeName) {
        MetricsResponse cubeMetrics = new MetricsResponse();
        Float totalCubeSize = 0f;
        long totalRecoadSize = 0;
        List<CubeInstance> cubeInstances = cubeService.listAllCubes(cubeName, projectName, null, true);
        Integer totalCube = cubeInstances.size();
        if (projectName == null) {
            totalCube += getHybridManager().listHybridInstances().size();
        } else {
            ProjectInstance project = getProjectManager().getProject(projectName);
            totalCube += project.getRealizationCount(RealizationType.HYBRID);
        }
        Float minCubeExpansion = Float.POSITIVE_INFINITY;
        Float maxCubeExpansion = Float.NEGATIVE_INFINITY;
        cubeMetrics.increase("totalCube", totalCube.floatValue());
        for (CubeInstance cubeInstance : cubeInstances) {
            if (cubeInstance.getInputRecordSizeBytes() > 0) {
                totalCubeSize += cubeInstance.getSizeKB();
                totalRecoadSize += cubeInstance.getInputRecordSizeBytes();
                Float cubeExpansion = new Float(cubeInstance.getSizeKB()) * 1024
                        / cubeInstance.getInputRecordSizeBytes();
                if (cubeExpansion > maxCubeExpansion) {
                    maxCubeExpansion = cubeExpansion;
                }
                if (cubeExpansion < minCubeExpansion) {
                    minCubeExpansion = cubeExpansion;
                }
            }
        }
        Float avgCubeExpansion = 0f;
        if (totalRecoadSize != 0) {
            avgCubeExpansion = totalCubeSize * 1024 / totalRecoadSize;
        }
        cubeMetrics.increase("avgCubeExpansion", avgCubeExpansion);
        cubeMetrics.increase("maxCubeExpansion", maxCubeExpansion == Float.NEGATIVE_INFINITY ? 0 : maxCubeExpansion);
        cubeMetrics.increase("minCubeExpansion", minCubeExpansion == Float.POSITIVE_INFINITY ? 0 : minCubeExpansion);
        return cubeMetrics;
    }

    private List<CubeInstance> getCubeByHybrid(HybridInstance hybridInstance) {
        List<CubeInstance> cubeInstances = Lists.newArrayList();
        List<RealizationEntry> realizationEntries = hybridInstance.getRealizationEntries();
        for (RealizationEntry realizationEntry : realizationEntries) {
            String reName = realizationEntry.getRealization();
            if (RealizationType.CUBE == realizationEntry.getType()) {
                CubeInstance cubeInstance = getCubeManager().getCube(reName);
                cubeInstances.add(cubeInstance);
            } else if (RealizationType.HYBRID == realizationEntry.getType()) {
                HybridInstance innerHybridInstance = getHybridManager().getHybridInstance(reName);
                cubeInstances.addAll(getCubeByHybrid(innerHybridInstance));
            }
        }
        return cubeInstances;
    };

    public PrepareSqlRequest getQueryMetricsSQLRequest(String startTime, String endTime, String projectName,
            String cubeName) {
        String[] metrics = new String[] { QueryMetricEnum.QUERY_COUNT.toSQL(),
                QueryMetricEnum.AVG_QUERY_LATENCY.toSQL(), QueryMetricEnum.MAX_QUERY_LATENCY.toSQL(),
                QueryMetricEnum.MIN_QUERY_LATENCY.toSQL() };
        Map<String, String> filterMap = getBaseFilterMap(CategoryEnum.QUERY, projectName, startTime, endTime);
        filterMap.putAll(getCubeFilterMap(CategoryEnum.QUERY, cubeName));
        return createPrepareSqlRequest(null, metrics,
                getMetricsManager().getSystemTableFromSubject(getConfig().getKylinMetricsSubjectQuery()), filterMap);
    };

    public PrepareSqlRequest getJobMetricsSQLRequest(String startTime, String endTime, String projectName,
            String cubeName) {
        String[] metrics = new String[] { JobMetricEnum.JOB_COUNT.toSQL(), JobMetricEnum.AVG_JOB_BUILD_TIME.toSQL(),
                JobMetricEnum.MAX_JOB_BUILD_TIME.toSQL(), JobMetricEnum.MIN_JOB_BUILD_TIME.toSQL() };
        Map<String, String> filterMap = getBaseFilterMap(CategoryEnum.JOB, projectName, startTime, endTime);
        filterMap.putAll(getCubeFilterMap(CategoryEnum.JOB, cubeName));
        return createPrepareSqlRequest(null, metrics,
                getMetricsManager().getSystemTableFromSubject(getConfig().getKylinMetricsSubjectJob()), filterMap);
    }

    public PrepareSqlRequest getChartSQLRequest(String startTime, String endTime, String projectName, String cubeName,
            String dimension, String metric, String category) {
        try {
            CategoryEnum categoryEnum = CategoryEnum.valueOf(category);
            String table = "";
            String[] dimensionSQL = null;
            String[] metricSQL = null;

            if (categoryEnum == CategoryEnum.QUERY) {
                dimensionSQL = new String[] { QueryDimensionEnum.valueOf(dimension).toSQL() };
                metricSQL = new String[] { QueryMetricEnum.valueOf(metric).toSQL() };
                table = getMetricsManager().getSystemTableFromSubject(getConfig().getKylinMetricsSubjectQuery());
            } else if (categoryEnum == CategoryEnum.JOB) {
                dimensionSQL = new String[] { JobDimensionEnum.valueOf(dimension).toSQL() };
                metricSQL = new String[] { JobMetricEnum.valueOf(metric).toSQL() };
                table = getMetricsManager().getSystemTableFromSubject(getConfig().getKylinMetricsSubjectJob());
            }

            Map<String, String> filterMap = getBaseFilterMap(categoryEnum, projectName, startTime, endTime);
            filterMap.putAll(getCubeFilterMap(categoryEnum, cubeName));

            return createPrepareSqlRequest(dimensionSQL, metricSQL, table, filterMap);
        } catch (IllegalArgumentException e) {
            String message = "Generate dashboard chart sql failed. Please double check the input parameter: dimension, metric or category.";
            logger.error(message, e);
            throw new BadRequestException(message + " Caused by: " + e.getMessage(), null, e.getCause());
        }
    }

    public MetricsResponse transformChartData(SQLResponse sqlResponse) {
        if (!sqlResponse.getIsException()) {
            MetricsResponse metrics = new MetricsResponse();
            List<List<String>> results = sqlResponse.getResults();
            for (List<String> result : results) {
                String dimension = result.get(0);
                if (dimension != null && !dimension.isEmpty()) {
                    String metric = result.get(1);
                    metrics.increase(dimension, getMetricValue(metric));
                }
            }
            return metrics;
        }
        return null;
    }

    public Float getMetricValue(String value) {
        if (value == null || value.isEmpty()) {
            return 0f;
        } else {
            return Float.valueOf(value);
        }
    }

    @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN + " or hasPermission(#project, 'ADMINISTRATION')")
    public void checkAuthorization(ProjectInstance project) throws AccessDeniedException {
    }

    @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN)
    public void checkAuthorization() throws AccessDeniedException {
    }

    private Map<String, String> getBaseFilterMap(CategoryEnum category, String projectName, String startTime,
            String endTime) {
        HashMap<String, String> filterMap = new HashMap<>();
        String project = "";
        if (category == CategoryEnum.QUERY) {
            project = QueryDimensionEnum.PROJECT.toSQL();
        } else {
            project = JobDimensionEnum.PROJECT.toSQL();
        }
        filterMap.put(TimePropertyEnum.DAY_DATE.toString() + " >= ?", startTime);
        filterMap.put(TimePropertyEnum.DAY_DATE.toString() + " <= ?", endTime);

        if (!Strings.isNullOrEmpty(projectName)) {
            filterMap.put(project + " = ?", projectName.toUpperCase(Locale.ROOT));
        } else {
            filterMap.put(project + " <> ?", MetricsManager.SYSTEM_PROJECT);
        }
        return filterMap;
    }

    private Map<String, String> getCubeFilterMap(CategoryEnum category, String cubeName) {
        HashMap<String, String> filterMap = new HashMap<>();

        if (category == CategoryEnum.QUERY) {
            filterMap.put(QueryPropertyEnum.EXCEPTION.toString() + " = ?", "NULL");

            if (!Strings.isNullOrEmpty(cubeName)) {
                filterMap.put(QueryPropertyEnum.REALIZATION + " = ?", cubeName);
            }
        } else if (category == CategoryEnum.JOB && !Strings.isNullOrEmpty(cubeName)) {
            HybridInstance hybridInstance = getHybridManager().getHybridInstance(cubeName);
            if (null != hybridInstance) {
                StringBuffer cubeNames = new StringBuffer();
                for (CubeInstance cube : getCubeByHybrid(hybridInstance)) {
                    cubeNames.append(",'" + cube.getName() + "'");
                }
                filterMap.put(JobPropertyEnum.CUBE.toString() + " IN (?)", cubeNames.substring(1));
            } else {
                filterMap.put(JobPropertyEnum.CUBE.toString() + " = ?", cubeName);
            }
        }
        return filterMap;
    }

    private PrepareSqlRequest createPrepareSqlRequest(String[] dimensions, String[] metrics, String category,
            Map<String, String> filterMap) {
        PrepareSqlRequest sqlRequest = new PrepareSqlRequest();
        sqlRequest.setProject(MetricsManager.SYSTEM_PROJECT);
        StringBuffer baseSQL = new StringBuffer("select ");
        StringBuffer groupBy = new StringBuffer("");
        if (dimensions != null && dimensions.length > 0) {
            groupBy.append(" group by ");
            StringBuffer dimensionSQL = new StringBuffer("");
            for (String dimension : dimensions) {
                dimensionSQL.append(",");
                dimensionSQL.append(dimension);
            }
            baseSQL.append(dimensionSQL.substring(1));
            groupBy.append(dimensionSQL.substring(1));
        }
        if (metrics != null && metrics.length > 0) {
            StringBuffer metricSQL = new StringBuffer("");
            for (String metric : metrics) {
                metricSQL.append(",");
                metricSQL.append(metric);
            }
            if (groupBy.length() > 0) {
                baseSQL.append(metricSQL);
            } else {
                baseSQL.append(metricSQL.substring(1));
            }
        }
        baseSQL.append(" from ");
        baseSQL.append(category);
        if (filterMap != null && filterMap.size() > 0) {
            PrepareSqlRequest.StateParam[] params = new PrepareSqlRequest.StateParam[filterMap.size()];
            int i = 0;
            StringBuffer filterSQL = new StringBuffer(" where ");
            Iterator<String> it = filterMap.keySet().iterator();
            String filter = it.next();
            filterSQL.append(filter);
            params[i] = new PrepareSqlRequest.StateParam();
            params[i].setClassName("java.lang.String");
            params[i++].setValue(filterMap.get(filter));

            while (it.hasNext()) {
                filter = it.next();
                filterSQL.append(" and ");
                filterSQL.append(filter);
                params[i] = new PrepareSqlRequest.StateParam();
                params[i].setClassName("java.lang.String");
                params[i++].setValue(filterMap.get(filter));
            }
            baseSQL.append(filterSQL.toString());
            sqlRequest.setParams(params);
        }
        baseSQL.append(groupBy);
        sqlRequest.setSql(baseSQL.toString());
        return sqlRequest;
    }

    private enum CategoryEnum {
        QUERY, JOB
    }

    private enum QueryDimensionEnum {
        PROJECT(QueryPropertyEnum.PROJECT.toString()), //
        CUBE(QueryPropertyEnum.REALIZATION.toString()), //
        DAY(TimePropertyEnum.DAY_DATE.toString()), //
        WEEK(TimePropertyEnum.WEEK_BEGIN_DATE.toString()), //
        MONTH(TimePropertyEnum.MONTH.toString());

        private final String sql;

        QueryDimensionEnum(String sql) {
            this.sql = sql;
        }

        public String toSQL() {
            return this.sql;
        }
    }

    private enum JobDimensionEnum {
        PROJECT(JobPropertyEnum.PROJECT.toString()), //
        CUBE(JobPropertyEnum.CUBE.toString()), //
        DAY(TimePropertyEnum.DAY_DATE.toString()), //
        WEEK(TimePropertyEnum.WEEK_BEGIN_DATE.toString()), //
        MONTH(TimePropertyEnum.MONTH.toString());

        private final String sql;

        JobDimensionEnum(String sql) {
            this.sql = sql;
        }

        public String toSQL() {
            return this.sql;
        }
    }

    private enum QueryMetricEnum {
        QUERY_COUNT("count(*)"), //
        AVG_QUERY_LATENCY("avg(" + QueryPropertyEnum.TIME_COST.toString() + ")"), //
        MAX_QUERY_LATENCY("max(" + QueryPropertyEnum.TIME_COST.toString() + ")"), //
        MIN_QUERY_LATENCY("min(" + QueryPropertyEnum.TIME_COST.toString() + ")");

        private final String sql;

        QueryMetricEnum(String sql) {
            this.sql = sql;
        }

        public String toSQL() {
            return this.sql;
        }
    }

    private enum JobMetricEnum {
        JOB_COUNT("count(*)"), //
        AVG_JOB_BUILD_TIME("avg(" + JobPropertyEnum.PER_BYTES_TIME_COST.toString() + ")"), //
        MAX_JOB_BUILD_TIME("max(" + JobPropertyEnum.PER_BYTES_TIME_COST.toString() + ")"), //
        MIN_JOB_BUILD_TIME("min(" + JobPropertyEnum.PER_BYTES_TIME_COST.toString() + ")");

        private final String sql;

        JobMetricEnum(String sql) {
            this.sql = sql;
        }

        public String toSQL() {
            return this.sql;
        }
    }
}