package cz.senslog.connector.fetch.fieldclimate;

import com.google.gson.reflect.TypeToken;
import cz.senslog.connector.fetch.api.ConnectorFetcher;
import cz.senslog.connector.model.config.HostConfig;
import cz.senslog.connector.tools.exception.ParseException;
import cz.senslog.connector.tools.exception.SyntaxException;
import cz.senslog.connector.fetch.fieldclimate.auth.AuthenticationService;
import cz.senslog.connector.tools.http.HttpClient;
import cz.senslog.connector.tools.http.HttpRequest;
import cz.senslog.connector.tools.http.HttpResponse;
import cz.senslog.connector.tools.http.URLBuilder;
import cz.senslog.connector.model.api.ProxySessionModel;
import cz.senslog.connector.model.fieldclimate.*;
import cz.senslog.connector.tools.util.Tuple;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.lang.reflect.Type;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;

import static cz.senslog.connector.tools.http.HttpContentType.APPLICATION_JSON;
import static cz.senslog.connector.tools.http.HttpHeader.*;
import static cz.senslog.connector.tools.http.HttpMethod.GET;
import static cz.senslog.connector.tools.json.BasicJson.jsonToObject;
import static cz.senslog.connector.tools.util.StringUtils.isBlank;
import static java.lang.String.format;
import static java.time.OffsetDateTime.MAX;
import static java.time.OffsetDateTime.MIN;
import static java.time.ZoneOffset.UTC;
import static java.time.format.DateTimeFormatter.*;
import static java.util.Collections.emptyList;
import static org.apache.hc.client5.http.utils.DateUtils.FORMATTER_RFC1123;
import static org.apache.hc.client5.http.utils.DateUtils.formatDate;

public class FieldClimateFetcher implements ConnectorFetcher<FieldClimateSession, FieldClimateModel> {

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

    private final AuthenticationService authService;
    private final FieldClimateConfig config;
    private final HttpClient httpClient;

    private List<StationInfo> stationInfos;
    private FieldClimateSession localSession;

    public FieldClimateFetcher(FieldClimateConfig config, AuthenticationService authService, HttpClient httpClient) {
        this.authService = authService;
        this.config = config;
        this.httpClient = httpClient;
        this.localSession = FieldClimateSession.emptySession();
    }

    public FieldClimateFetcher() { this(null, null, null); }

    @Override
    public void init() throws Exception {
        assert config != null; assert  authService != null; assert  httpClient != null;

        String requestDate = formatDate(new Date().toInstant(), FORMATTER_RFC1123);
        HostConfig stationsHost = config.getStationsHost();
        String seed = "GET" + stationsHost.getPath() + requestDate;

        HttpRequest request = HttpRequest.newBuilder().GET()
                .url(URLBuilder.newBuilder(stationsHost.getDomain(), stationsHost.getPath()).build())
                .header(ACCEPT, APPLICATION_JSON)
                .header(AUTHORIZATION, authService.getAccessToken(seed))
                .header(DATE, requestDate)
                .build();
        logger.info("Creating a http request to {}.", request);

        HttpResponse response = httpClient.send(request);

        if (response.isError()) {
            throw logger.throwing(new Exception(format(
                    "Can not get information about the stations. %s", response.getBody()
            )));
        }

        List<StationInfo> stations;
        try {
            logger.debug("Parsing body of the response to the class {}.", StationInfo.class);
            Type sensorInfoListType = new TypeToken<Collection<StationInfo>>() {}.getType();
            stations = jsonToObject(response.getBody(), sensorInfoListType,
                    Tuple.of(LocalDateTime.class, s -> LocalDateTime.parse(s, ofPattern("yyyy-MM-dd HH:mm:ss")))
            );
        } catch (SyntaxException e) {
            throw logger.throwing(new Exception(format(
                    "JSON response can not be parsed to the list of stations. %s", e.getMessage()
            )));
        }

        if (stations.isEmpty()) {
            throw logger.throwing(new Exception("Received empty list of stations."));
        }

        stationInfos = new ArrayList<>(stations.size());
        for (StationInfo station : stations) {
            if (config.getAllowedStation().isAllowed(station.getName().getOriginal())) {
                stationInfos.add(station);
            }
        }

        logger.info("{} stations were loaded.", stationInfos.size());
        logger.info(stationInfos.toString());
    }

    @Override
    public FieldClimateModel fetch(Optional<FieldClimateSession> persistenceSession) {
        assert config != null; assert  authService != null; assert  httpClient != null;

        FieldClimateSession session = persistenceSession.filter(ProxySessionModel::isActive).orElse(localSession);
        ZoneId zoneId = config.getTimeZone().toZoneId();
        OffsetDateTime startDate = ZonedDateTime.of(config.getStartDate(), zoneId).toOffsetDateTime();
        ZoneOffset zoneOffset = startDate.getOffset();

        OffsetDateTime globalFrom = MAX, globalTo = MIN;
        List<StationData> stationDataList = new ArrayList<>(stationInfos.size());
        int totalFetched = 0;

        for (StationInfo station : stationInfos) {

            String stationId = station.getName().getOriginal();
            logger.info("Fetching data for the station {}.", stationId);

            if (isBlank(stationId)) {
                logger.warn("The station ID is empty, data can not be fetched."); continue;
            }

            OffsetDateTime firstValueDate = OffsetDateTime.of(station.getMinDate(), zoneOffset);
            OffsetDateTime lastFetch = session.getLastUnitTime(stationId, MIN);

            long fromDateEpoch = Math.max(startDate.toEpochSecond(), Math.max(firstValueDate.toEpochSecond(), lastFetch.toEpochSecond()));
            logger.debug("Getting a start date time for station {}.", stationId);
            OffsetDateTime localFrom = OffsetDateTime.ofInstant(Instant.ofEpochSecond(fromDateEpoch), zoneOffset);


            StationTimeRange timeRange = getTimeRangeForStation(stationId);
            if (timeRange == null) continue;


            OffsetDateTime localTo = localFrom.plusHours(config.getPeriod());
            OffsetDateTime stationMax = ZonedDateTime.of(timeRange.getMax_date(), config.getTimeZone().toZoneId()).toOffsetDateTime();
            if (localTo.isAfter(stationMax)) {
                localTo = stationMax;
            }

            if (localFrom.equals(localTo)) {
                logger.info("No new data for the station {} is available at this moment.", stationId); continue;
            }

            logger.info("New data for the station {} are going to be fetched from {} to {}.",
                    stationId, localFrom, localTo);

            HostConfig stationDataHost = config.getStationDataHost();
            logger.info("Creating a http request to {}.", stationDataHost);
            String path = stationDataHost.getPath()
                    .replace("{station_id}", stationId)
                    .replace("{from}", String.valueOf(localFrom.toLocalDateTime().toEpochSecond(UTC)))
                    .replace("{to}", String.valueOf(localTo.toLocalDateTime().toEpochSecond(UTC)));

            String requestDate = formatDate(new Date());
            String authToken = authService.getAccessToken(GET.name() + path + requestDate);
            HttpRequest request = HttpRequest.newBuilder().GET()
                    .url(URLBuilder.newBuilder(stationDataHost.getDomain(), path).build())
                    .header(ACCEPT, APPLICATION_JSON)
                    .header(DATE, requestDate)
                    .header(AUTHORIZATION, authToken)
                    .build();

            logger.info("Sending the http request: {}", request);
            HttpResponse response = httpClient.send(request);
            logger.info("Received a response with a status: {} for the domain {}.", response.getStatus(), stationDataHost.getDomain());


            if (response.isOk()) {

                StationData stationData;
                try {
                    logger.debug("Parsing body of the response to the class {}.", StationData.class);
                    stationData = jsonToObject(response.getBody(), StationData.class);
                    logger.info("Received {} records for the sensor {}.", stationData.getData().size(), stationId);
                } catch (SyntaxException e) {
                    logger.catching(e);
                    continue;
                }

                List<SensorDataInfo> filteredSensors = new ArrayList<>(stationData.getSensors().size());
                for (SensorDataInfo sensor : stationData.getSensors()) {
                    if (SensorType.of(sensor.getCode()).isChanneled()) {
                        if (config.getAllowedStation().isAllowed(stationId, sensor.getCode(), sensor.getCh())) {
                            filteredSensors.add(sensor);
                        }
                    } else if (config.getAllowedStation().isAllowed(stationId, sensor.getCode())) {
                        filteredSensors.add(sensor);
                    }
                }

                stationData.setId(stationId);
                stationData.setSensors(filteredSensors);

                OffsetDateTime latestTimestamp = OffsetDateTime.MIN;
                List<Map<String, String>> filteredDataList = new ArrayList<>(stationData.getData().size());
                for (Map<String, String> sensorDataMap : stationData.getData()) {
                    Map<String, String> filteredSensorData = new HashMap<>(sensorDataMap.size());

                    String strDate = sensorDataMap.getOrDefault("date", "");
                    final DateTimeFormatter formatter = ofPattern("yyyy-MM-dd HH:mm:ss");
                    LocalDateTime date = LocalDateTime.parse(strDate, formatter);
                    ZonedDateTime zonedDate = ZonedDateTime.of(date, config.getTimeZone().toZoneId());
                    OffsetDateTime timestamp = zonedDate.toOffsetDateTime();
                    filteredSensorData.put("date", timestamp.format(ISO_OFFSET_DATE_TIME));

                    for (SensorDataInfo sensor : filteredSensors) {
                        for (String aggrType : sensor.getAggr().keySet()) {
                            String sensorDataHash = format("%s_%s_%s_%s_%s",
                                    sensor.getCh(), sensor.getMac(), sensor.getSerial(), sensor.getCode(), aggrType);
                            if (sensorDataMap.containsKey(sensorDataHash)) {
                                filteredSensorData.put(sensorDataHash, sensorDataMap.get(sensorDataHash));
                            }
                        }
                    }

                    filteredDataList.add(filteredSensorData);

                    if (latestTimestamp.isBefore(timestamp)) {
                        latestTimestamp = timestamp;
                    }
                }
                stationData.setData(filteredDataList);

                localTo = latestTimestamp.isAfter(localTo) ? latestTimestamp : localTo;

                stationDataList.add(stationData);

                int records = stationData.getSensors().size();
                totalFetched += records;

                logger.info("Fetched {} records for the station {}.", records, stationId);

            } else {

                if (response.getStatus() >= 300) {
                    logger.error("Can not get data from the station {}. Error {} {}",
                            stationId, response.getStatus(), response.getBody());
                    continue;
                }
            }

            session.addLastUnitTime(stationId, localTo);

            if (globalFrom.isAfter(localFrom)) {
                  globalFrom = localFrom;
            }

            if (globalTo.isBefore(localTo)) {
                globalTo = localTo;
            }
        }


        List<StationData> dataList = totalFetched != 0 ? stationDataList : emptyList();
        OffsetDateTime now = OffsetDateTime.now();
        OffsetDateTime from = globalFrom == MAX ? now : globalFrom;
        OffsetDateTime to = globalTo == MIN ? now : globalTo;

        logger.info("Fetched {} records from {} to {}.",
                totalFetched, globalFrom.format(ISO_OFFSET_DATE_TIME), globalTo.format(ISO_OFFSET_DATE_TIME));

        return new FieldClimateModel(dataList, from, to);
    }

    private StationTimeRange getTimeRangeForStation(String stationId) {
        logger.debug("Getting an actual time range for the station {}.", stationId);
        String requestDate = formatDate(new Date());
        HostConfig timeRangeHost = config.getStationTimeRangeHost();
        String path = timeRangeHost.getPath().replace("{station_id}", stationId);
        String authToken =  authService.getAccessToken(GET.name() + path + requestDate);

        HttpRequest request = HttpRequest.newBuilder().GET()
                .url(URLBuilder.newBuilder(timeRangeHost.getDomain(), path).build())
                .header(ACCEPT, APPLICATION_JSON)
                .header(DATE, requestDate)
                .header(AUTHORIZATION, authToken)
                .build();

        logger.info("Sending the http request: {}", request);
        HttpResponse response = httpClient.send(request);
        logger.info("Received a response with a status: {}.", response.getStatus());

        if (response.isError()) {
            logger.error("Can not get data of the sensor {}. Error {}: {}",
                    stationId, response.getStatus(), response.getBody());
            return null;
        }

        StationTimeRange timeRange = null;
        try {
            logger.debug("Parsing body of the json response to the class {}.", StationTimeRange.class);
            timeRange = jsonToObject(response.getBody(), StationTimeRange.class,
                    Tuple.of(LocalDateTime.class,
                            el -> LocalDateTime.parse(el, ofPattern("yyyy-MM-dd HH:mm:ss")))
            );
        } catch (SyntaxException | ParseException e) {
            logger.catching(e);
        }

        return timeRange;
    }
}
