package cz.senslog.connector.fetch.drutes;

import cz.senslog.connector.fetch.api.ConnectorFetcher;
import cz.senslog.connector.model.api.VoidSession;
import cz.senslog.connector.model.drutes.DrutesModel;
import cz.senslog.connector.model.drutes.PredictionData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.lang.Double.parseDouble;
import static java.lang.Integer.parseInt;
import static java.lang.Long.parseLong;
import static java.time.LocalDateTime.parse;
import static java.time.format.DateTimeFormatter.ofPattern;
import static java.util.stream.Collectors.toCollection;

public class DrutesFetcher implements ConnectorFetcher<VoidSession, DrutesModel> {

    private static class DirInfo {
        final long unitId;
        final long sensorId;
        final String path;

        DirInfo(long unitId, long sensorId, String path) {
            this.path = path;
            this.unitId = unitId;
            this.sensorId = sensorId;
        }

        @Override
        public String toString() {
            return String.format("%d | %d => %s", unitId, sensorId, path);
        }
    }

    private static class PredictionInfo {
        final DirInfo dirInfo;
        final OffsetDateTime predictCreated;
        final OffsetDateTime predictFrom;
        final Path predictFile;

        PredictionInfo(DirInfo dirInfo, Path predictFile, OffsetDateTime predictCreated, OffsetDateTime predictFrom) {
            this.dirInfo = dirInfo;
            this.predictFrom = predictFrom;
            this.predictCreated = predictCreated;
            this.predictFile = predictFile;
        }
    }

    private static final Logger logger = LogManager.getLogger(DrutesFetcher.class);

    private static final ZoneId DEFAULT_ZONE = ZoneId.systemDefault();
    private static final String SUCCESS_IMPORT_FILE = "import.success";
    private static final String SUCCESS_SENSORS_FILE = "sensors.success";
    private static final String TIMES_PREDICT_FILE = "predict.times";
    private static final String PREDICT_FILE_EXTENSION = ".predict";

    private final DrutesConfig config;

    private List<DirInfo> sensorsToLoad;

    DrutesFetcher(DrutesConfig config) {
        this.config = config;
    }

    @Override
    public void init() throws Exception {
        if (Files.notExists(Paths.get(config.getBaseDir()))) {
            throw logger.throwing(new IOException(String.format(
                    "Provided base dir '%s' does not exist.", config.getBaseDir()
            )));
        }

        try (Stream<Path> paths = Files.walk(Paths.get(config.getBaseDir()))) {
            final Pattern numericPattern = Pattern.compile("-?\\d+(\\.\\d+)?");
            sensorsToLoad = paths.filter(Files::isDirectory)
                    .filter(p -> numericPattern.matcher(p.getFileName().toString()).matches())
                    .map(p -> new DirInfo(
                            config.getUnitId(), parseLong(p.getFileName().toString()),
                            Paths.get(p.toString(), config.getObservedDir()).toString())
                    ).collect(Collectors.toList());

        } catch (IOException e) {
            sensorsToLoad = Collections.emptyList();
            logger.catching(e);
        }
    }

    @Override
    public DrutesModel fetch(Optional<VoidSession> session) {

        List<PredictionInfo> predictionInfos = sensorsToLoad.parallelStream()
                .filter(dir -> isSuccessFile(Paths.get(dir.path, SUCCESS_SENSORS_FILE)))
                .map(dir -> {
                    List<String> timesInfo = getFileContent(Paths.get(dir.path, TIMES_PREDICT_FILE));
                    if (timesInfo.size() < 4) {
                        logger.error("The file '{}' contains wrong content.", TIMES_PREDICT_FILE); return null;
                    }
                    OffsetDateTime predictFrom = parse(timesInfo.get(3), ofPattern("yyyy-MM-dd HH:mm:ss")).atZone(DEFAULT_ZONE).toOffsetDateTime();
                    Path predictFile = Paths.get(dir.path, dir.sensorId + PREDICT_FILE_EXTENSION);
                    BasicFileAttributes fileAttributes = fileAttributes(predictFile);
                    if (fileAttributes == null) {
                        logger.error("Creating time of the file '{}' can not be loaded.", predictFile); return null;
                    }
                    OffsetDateTime predictionCreated = fileAttributes.creationTime().toInstant().atZone(DEFAULT_ZONE).toOffsetDateTime();
                    return new PredictionInfo(dir, predictFile, predictionCreated, predictFrom);
                }).filter(Objects::nonNull).collect(Collectors.toList());

        List<PredictionData> predictions = Collections.synchronizedList(new LinkedList<>());
        for (PredictionInfo info : predictionInfos) {
            Path successFilePath = Paths.get(info.dirInfo.path, SUCCESS_IMPORT_FILE);
            try {
                Files.readAllLines(info.predictFile).stream().skip(1)
                        .map(l -> parseFileLine(l, info)).filter(Objects::nonNull)
                        .collect(toCollection(() -> predictions));
                createSuccessFile(successFilePath, true);
            } catch (IOException e) {
                logger.catching(e);
                createSuccessFile(successFilePath, false);
            }
        }

        if (predictions.isEmpty()) {
            logger.warn("No prediction were loaded.");
            return new DrutesModel(Collections.emptyList(), null, null);
        }

        PredictionData lastPredict = predictions.get(predictions.size()-1);
        OffsetDateTime from = lastPredict.getPredictCreated();
        OffsetDateTime to = from.plusSeconds(lastPredict.getTimeshift());
        logger.info("Loaded {} prediction from '{}' to '{}'.", predictions.size(), from, to);

        DrutesModel model = new DrutesModel(predictions, from, to);
        logger.debug("Model was created successfully and sending it forward.");

        return model;
    }

    private static BasicFileAttributes fileAttributes(Path filePath) {
        if (Files.notExists(filePath)) {
            logger.warn("The file '{}' does not exist.", filePath); return null;
        }
        try {
            return Files.readAttributes(filePath, BasicFileAttributes.class);
        } catch (IOException e) {
            logger.catching(e); return null;
        }
    }

    private static List<String> getFileContent(Path filePath) {
        if (Files.exists(filePath)) {
            try {
                return Files.readAllLines(filePath);
            } catch (IOException e) {
                logger.catching(e);
            }
        }
        return Collections.emptyList();
    }

    private static boolean isSuccessFile(Path successFilePath)  {
        List<String> lines = getFileContent(successFilePath);
        if (lines.isEmpty() || parseInt(lines.get(0).trim()) == 0) {
            logger.warn("The prediction '{}' was not successfully created.", successFilePath);
            return false;
        }
        return true;
    }

    private static void createSuccessFile(Path filePath, boolean success) {
        try {
            String content = success ? "1" : "0";
            Files.write(filePath, content.getBytes());
        } catch (IOException e) {
            logger.catching(e);
        }
    }

    private static PredictionData parseFileLine(String line, PredictionInfo info) {
        String [] cmp = line.trim().split("\\s+");
        long timeshift;
        double [] values;
        if (cmp.length == 2) {
             timeshift = new BigDecimal(cmp[0].toUpperCase()).longValue();
            values = new double[] { parseDouble(cmp[1]) };
        } else if (cmp.length == 3) {
            timeshift = new BigDecimal(cmp[0].toUpperCase()).longValue();
            values = new double[] { parseDouble(cmp[1]), parseDouble(cmp[2]) };
        } else {
            logger.error("The parser does not exist for the data: '{}'.", line);
            return null;
        }
        return new PredictionData(info.dirInfo.unitId, info.dirInfo.sensorId,
                timeshift, values, info.predictCreated, info.predictFrom
        );
    }
}
