package com.njcn.influxdb.mapper;

import io.swagger.models.auth.In;
import org.influxdb.InfluxDBMapperException;
import org.influxdb.annotation.Column;
import org.influxdb.annotation.Measurement;
import org.influxdb.dto.QueryResult;

import java.lang.reflect.Field;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

/**
 * pqs
 * 用于处理格式化时间
 * @author cdf
 * @date 2022/8/24
 */
public class InfluxDBResultMapperCn {


        /**
         * Data structure used to cache classes used as measurements.
         */
        private static final
        ConcurrentMap<String, ConcurrentMap<String, Field>> CLASS_FIELD_CACHE = new ConcurrentHashMap<>();





        /**
         * <p>
         * Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal
         * data structure and creating the respective object instances based on the Class passed as
         * parameter.
         * </p>
         *
         * @param queryResult the InfluxDB result object
         * @param clazz the Class that will be used to hold your measurement data
         * @param <T> the target type
         *
         * @return a {@link List} of objects from the same Class passed as parameter and sorted on the
         * same order as received from InfluxDB.
         *
         * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors,
         * <code>clazz</code> parameter is not annotated with &#64;Measurement or it was not
         * possible to define the values of your POJO (e.g. due to an unsupported field type).
         */
        public <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz) throws InfluxDBMapperException {
            return toPOJO(queryResult, clazz, TimeUnit.MILLISECONDS);
        }

        /**
         * <p>
         * Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal
         * data structure and creating the respective object instances based on the Class passed as
         * parameter.
         * </p>
         *
         * @param queryResult the InfluxDB result object
         * @param clazz the Class that will be used to hold your measurement data
         * @param precision the time precision of results
         * @param <T> the target type
         *
         * @return a {@link List} of objects from the same Class passed as parameter and sorted on the
         * same order as received from InfluxDB.
         *
         * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors,
         * <code>clazz</code> parameter is not annotated with &#64;Measurement or it was not
         * possible to define the values of your POJO (e.g. due to an unsupported field type).
         */
        public <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz,
                                  final TimeUnit precision) throws InfluxDBMapperException {
            throwExceptionIfMissingAnnotation(clazz);
            String measurementName = getMeasurementName(clazz);
            return this.toPOJO(queryResult, clazz, measurementName, precision);
        }

        /**
         * <p>
         * Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal
         * data structure and creating the respective object instances based on the Class passed as
         * parameter.
         * </p>
         *
         * @param queryResult the InfluxDB result object
         * @param clazz the Class that will be used to hold your measurement data
         * @param <T> the target type
         * @param measurementName name of the Measurement
         *
         * @return a {@link List} of objects from the same Class passed as parameter and sorted on the
         * same order as received from InfluxDB.
         *
         * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors,
         * <code>clazz</code> parameter is not annotated with &#64;Measurement or it was not
         * possible to define the values of your POJO (e.g. due to an unsupported field type).
         */
        public <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz, final String measurementName)
                throws InfluxDBMapperException {
            return toPOJO(queryResult, clazz, measurementName, TimeUnit.MILLISECONDS);
        }

        /**
         * <p>
         * Process a {@link QueryResult} object returned by the InfluxDB client inspecting the internal
         * data structure and creating the respective object instances based on the Class passed as
         * parameter.
         * </p>
         *
         * @param queryResult the InfluxDB result object
         * @param clazz the Class that will be used to hold your measurement data
         * @param <T> the target type
         * @param measurementName name of the Measurement
         * @param precision the time precision of results
         *
         * @return a {@link List} of objects from the same Class passed as parameter and sorted on the
         * same order as received from InfluxDB.
         *
         * @throws InfluxDBMapperException If {@link QueryResult} parameter contain errors,
         * <code>clazz</code> parameter is not annotated with &#64;Measurement or it was not
         * possible to define the values of your POJO (e.g. due to an unsupported field type).
         */
        public <T> List<T> toPOJO(final QueryResult queryResult, final Class<T> clazz, final String measurementName,
                                  final TimeUnit precision)
                throws InfluxDBMapperException {

            Objects.requireNonNull(measurementName, "measurementName");
            Objects.requireNonNull(queryResult, "queryResult");
            Objects.requireNonNull(clazz, "clazz");

            throwExceptionIfResultWithError(queryResult);
            cacheMeasurementClass(clazz);

            List<T> result = new LinkedList<T>();

            queryResult.getResults().stream()
                    .filter(internalResult -> Objects.nonNull(internalResult) && Objects.nonNull(internalResult.getSeries()))
                    .forEach(internalResult -> {
                        internalResult.getSeries().stream()
                                .filter(series -> series.getName().equals(measurementName))
                                .forEachOrdered(series -> {
                                    parseSeriesAs(series, clazz, result, precision);
                                });
                    });

            return result;
        }

        void throwExceptionIfMissingAnnotation(final Class<?> clazz) {
            if (!clazz.isAnnotationPresent(Measurement.class)) {
                throw new IllegalArgumentException(
                        "Class " + clazz.getName() + " is not annotated with @" + Measurement.class.getSimpleName());
            }
        }

        void throwExceptionIfResultWithError(final QueryResult queryResult) {
            if (queryResult.getError() != null) {
                throw new InfluxDBMapperException("InfluxDB returned an error: " + queryResult.getError());
            }

            queryResult.getResults().forEach(seriesResult -> {
                if (seriesResult.getError() != null) {
                    throw new InfluxDBMapperException("InfluxDB returned an error with Series: " + seriesResult.getError());
                }
            });
        }

        ConcurrentMap<String, Field> getColNameAndFieldMap(final Class<?> clazz) {
            return CLASS_FIELD_CACHE.get(clazz.getName());
        }

        void cacheMeasurementClass(final Class<?>... classVarAgrs) {
            for (Class<?> clazz : classVarAgrs) {
                if (CLASS_FIELD_CACHE.containsKey(clazz.getName())) {
                    continue;
                }
                ConcurrentMap<String, Field> initialMap = new ConcurrentHashMap<>();
                ConcurrentMap<String, Field> influxColumnAndFieldMap = CLASS_FIELD_CACHE.putIfAbsent(clazz.getName(), initialMap);
                if (influxColumnAndFieldMap == null) {
                    influxColumnAndFieldMap = initialMap;
                }

                Class<?> c = clazz;
                while (c != null) {
                    for (Field field : c.getDeclaredFields()) {
                        Column colAnnotation = field.getAnnotation(Column.class);
                        if (colAnnotation != null) {
                            influxColumnAndFieldMap.put(colAnnotation.name(), field);
                        }
                    }
                    c = c.getSuperclass();
                }
            }
        }

        String getMeasurementName(final Class<?> clazz) {
            return ((Measurement) clazz.getAnnotation(Measurement.class)).name();
        }

        String getDatabaseName(final Class<?> clazz) {
            return ((Measurement) clazz.getAnnotation(Measurement.class)).database();
        }

        String getRetentionPolicy(final Class<?> clazz) {
            return ((Measurement) clazz.getAnnotation(Measurement.class)).retentionPolicy();
        }

        TimeUnit getTimeUnit(final Class<?> clazz) {
            return ((Measurement) clazz.getAnnotation(Measurement.class)).timeUnit();
        }

        <T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz, final List<T> result) {
            return parseSeriesAs(series, clazz, result, TimeUnit.MILLISECONDS);
        }

        <T> List<T> parseSeriesAs(final QueryResult.Series series, final Class<T> clazz, final List<T> result,
                                  final TimeUnit precision) {
            int columnSize = series.getColumns().size();
            ConcurrentMap<String, Field> colNameAndFieldMap = CLASS_FIELD_CACHE.get(clazz.getName());
            try {
                T object = null;
                for (List<Object> row : series.getValues()) {
                    for (int i = 0; i < columnSize; i++) {
                        Field correspondingField = colNameAndFieldMap.get(series.getColumns().get(i)/*InfluxDB columnName*/);
                        if (correspondingField != null) {
                            if (object == null) {
                                object = clazz.newInstance();
                            }
                            setFieldValue(object, correspondingField, row.get(i), precision);
                        }
                    }
                    // When the "GROUP BY" clause is used, "tags" are returned as Map<String,String> and
                    // accordingly with InfluxDB documentation
                    // https://docs.influxdata.com/influxdb/v1.2/concepts/glossary/#tag-value
                    // "tag" values are always String.
                    if (series.getTags() != null && !series.getTags().isEmpty()) {
                        for (Map.Entry<String, String> entry : series.getTags().entrySet()) {
                            Field correspondingField = colNameAndFieldMap.get(entry.getKey()/*InfluxDB columnName*/);
                            if (correspondingField != null) {
                                // I don't think it is possible to reach here without a valid "object"
                                setFieldValue(object, correspondingField, entry.getValue(), precision);
                            }
                        }
                    }
                    if (object != null) {
                        result.add(object);
                        object = null;
                    }
                }
            } catch (InstantiationException | IllegalAccessException e) {
                throw new InfluxDBMapperException(e);
            }
            return result;
        }

        /**
         * InfluxDB client returns any number as Double.
         * See https://github.com/influxdata/influxdb-java/issues/153#issuecomment-259681987
         * for more information.
         *
         * @param object
         * @param field
         * @param value
         * @param precision
         * @throws IllegalArgumentException
         * @throws IllegalAccessException
         */
        <T> void setFieldValue(final T object, final Field field, final Object value, final TimeUnit precision)
                throws IllegalArgumentException, IllegalAccessException {
            if (value == null) {
                return;
            }
            Class<?> fieldType = field.getType();
            try {
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                if (fieldValueModified(fieldType, field, object, value, precision)
                        || fieldValueForPrimitivesModified(fieldType, field, object, value)
                        || fieldValueForPrimitiveWrappersModified(fieldType, field, object, value)) {
                    return;
                }
                String msg = "Class '%s' field '%s' is from an unsupported type '%s'.";
                throw new InfluxDBMapperException(
                        String.format(msg, object.getClass().getName(), field.getName(), field.getType()));
            } catch (ClassCastException e) {
                String msg = "Class '%s' field '%s' was defined with a different field type and caused a ClassCastException. "
                        + "The correct type is '%s' (current field value: '%s').";
                throw new InfluxDBMapperException(
                        String.format(msg, object.getClass().getName(), field.getName(), value.getClass().getName(), value));
            }
        }

        <T> boolean fieldValueModified(final Class<?> fieldType, final Field field, final T object, final Object value,
                                       final TimeUnit precision)
                throws IllegalArgumentException, IllegalAccessException {
            if (String.class.isAssignableFrom(fieldType)) {
                if("timeId".equals(field.getName()) || "time".equals(field.getName())){
                    LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.from(DateTimeFormatter.ISO_DATE_TIME.parse(String.valueOf(value))), ZoneId.systemDefault());
                    String time = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
                    field.set(object,time);
                }else {
                    field.set(object, String.valueOf(value));
                }

                return true;
            }
            if (Instant.class.isAssignableFrom(fieldType)) {
                Instant instant;
                if (value instanceof String) {
                    instant = Instant.from(DateTimeFormatter.ISO_DATE_TIME.parse(String.valueOf(value)));
                } else if (value instanceof Long) {
                    instant = Instant.ofEpochMilli(toMillis((long) value, precision));
                } else if (value instanceof Double) {
                    instant = Instant.ofEpochMilli(toMillis(((Double) value).longValue(), precision));
                } else if (value instanceof Integer) {
                    instant = Instant.ofEpochMilli(toMillis(((Integer) value).longValue(), precision));
                } else {
                    throw new InfluxDBMapperException("Unsupported type " + field.getClass() + " for field " + field.getName());
                }
                field.set(object, instant);
                return true;
            }

            if (LocalDateTime.class.isAssignableFrom(fieldType)) {
                LocalDateTime localDateTime;
                if (value instanceof String) {
                    localDateTime = LocalDateTime.ofInstant(Instant.from(DateTimeFormatter.ISO_DATE_TIME.parse(String.valueOf(value))), ZoneId.systemDefault());
                } else {
                    throw new InfluxDBMapperException("Unsupported type " + field.getClass() + " for field " + field.getName());
                }
                field.set(object, localDateTime);
                return true;
            }

            return false;
        }

        <T> boolean fieldValueForPrimitivesModified(final Class<?> fieldType, final Field field, final T object,
                                                    final Object value) throws IllegalArgumentException, IllegalAccessException {
            if (double.class.isAssignableFrom(fieldType)) {
                field.setDouble(object, ((Double) value).doubleValue());
                return true;
            }
            if (long.class.isAssignableFrom(fieldType)) {
                field.setLong(object, ((Double) value).longValue());
                return true;
            }
            if (int.class.isAssignableFrom(fieldType)) {
                field.setInt(object, ((Double) value).intValue());
                return true;
            }
            if (boolean.class.isAssignableFrom(fieldType)) {
                field.setBoolean(object, Boolean.valueOf(String.valueOf(value)).booleanValue());
                return true;
            }
            return false;
        }

        <T> boolean fieldValueForPrimitiveWrappersModified(final Class<?> fieldType, final Field field, final T object,
                                                           final Object value) throws IllegalArgumentException, IllegalAccessException {
            if (Double.class.isAssignableFrom(fieldType)) {
                field.set(object, value);
                return true;
            }
            if (Long.class.isAssignableFrom(fieldType)) {
                field.set(object, Long.valueOf(((Double) value).longValue()));
                return true;
            }
            if (Integer.class.isAssignableFrom(fieldType)) {
                field.set(object, Integer.valueOf(((Double) value).intValue()));
                return true;
            }
            if (Boolean.class.isAssignableFrom(fieldType)) {
                field.set(object, Boolean.valueOf(String.valueOf(value)));
                return true;
            }
            return false;
        }

        private Long toMillis(final long value, final TimeUnit precision) {

            return TimeUnit.MILLISECONDS.convert(value, precision);
        }


}
