package cz.senslog.connector.fetch.azure;

import com.google.gson.reflect.TypeToken;
import cz.senslog.connector.tools.exception.ModuleInterruptedException;
import cz.senslog.connector.tools.exception.SyntaxException;
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.tools.json.JsonSchema;
import cz.senslog.connector.fetch.api.ConnectorFetcher;
import cz.senslog.connector.fetch.azure.auth.AuthenticationService;
import cz.senslog.connector.model.api.ProxySessionModel;
import cz.senslog.connector.model.azure.AzureModel;
import cz.senslog.connector.model.azure.SensorData;
import cz.senslog.connector.model.azure.SensorInfo;
import cz.senslog.connector.model.config.HostConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.lang.reflect.Type;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;

import static cz.senslog.connector.tools.http.HttpHeader.AUTHORIZATION;
import static cz.senslog.connector.tools.json.BasicJson.jsonToObject;
import static cz.senslog.connector.tools.util.StringUtils.isBlank;
import static cz.senslog.connector.model.converter.AzureUnitConverter.convertEuiToUnitId;
import static java.lang.String.format;
import static java.time.OffsetDateTime.MAX;
import static java.time.OffsetDateTime.MIN;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import static java.util.Collections.emptyList;

/**
 * The class {@code AzureFetcher} represents an implementation of {@link ConnectorFetcher}.
 * The class contains {@link AzureModel} which contains fetched data.
 *
 * <h2>Initialization</h2>
 * At first are loaded both schemas for validation received JSONs.
 * When schemas were loaded successfully, is triggered the authentication process to get access token.
 * The token is used to get information of sensors. If loading fails or are not get any sensors,
 * will be throw an exception. Otherwise the sensors are saved.
 *
 * <h2>Fetch</h2>
 * If list of sensors is empty, is returned empty model.
 * For each sensor are get data which is valid by schema and parsed from json format to object.
 * If the process is successful, data is saved to the sensor and is get the time of last update.
 * All data of sensors is send in model and the time of last fetch is changed according to time of the last update.
 *
 * @author Lukas Cerny
 * @version 1.0
 * @since 1.0
 */
public class AzureFetcher implements ConnectorFetcher<AzureSession, AzureModel> {

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


    /** Name of the sensor info JSON schema. */
    private static final String SENSOR_INFO_JSON_SCHEMA_NAME = "schema/sensorInfoSchema.json";

//    /** Name of the sensor data JSON schema. */
//    private static final String SENSOR_DATA_JSON_SCHEMA_NAME = "schema/sensorDataSchema.json";


    /** Configuration. */
    private final AzureConfig config;

    /** Service for authentication. */
    private final AuthenticationService authService;

    /** Http client. */
    private final HttpClient httpClient;

    /** List of sensor information. */
    private final List<SensorInfo> sensorInfos;

//    /** Schema for sensor data. */
//    private JsonSchema sensorDataSchema;

    private final AzureSession localSession;

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

    /**
     * Constructor of the class sets all attributes.
     * @param config - configuration for fetcher.
     * @param authService - authentication service.
     * @param httpClient - http client.
     */
    AzureFetcher(AzureConfig config, AuthenticationService authService, HttpClient httpClient) {
        this.config = config;
        this.authService = authService;
        this.httpClient = httpClient;
        this.sensorInfos = new ArrayList<>();
        this.localSession = AzureSession.emptySession();
    }

    @Override
    public void init() throws Exception {

//        logger.debug("Creating a new schema {}.", SENSOR_DATA_JSON_SCHEMA_NAME);
//        sensorDataSchema = JsonSchema.loadAsResource(SENSOR_DATA_JSON_SCHEMA_NAME);
//        logger.info("Schema {} was created successfully.", SENSOR_DATA_JSON_SCHEMA_NAME);

        logger.debug("Creating a new schema {}.", SENSOR_INFO_JSON_SCHEMA_NAME);
        JsonSchema sensorInfoSchema = JsonSchema.loadAsResource(SENSOR_INFO_JSON_SCHEMA_NAME);
        logger.info("Schema {} was created successfully.", SENSOR_INFO_JSON_SCHEMA_NAME);

        String accessToken = authService.getAccessToken();

        HostConfig sensorInfoHost = config.getSensorInfoHost();
        logger.info("Creating a http request to {}.", sensorInfoHost);
        HttpRequest request = HttpRequest.newBuilder().GET()
                .url(URLBuilder.newBuilder(sensorInfoHost.getDomain(), sensorInfoHost.getPath()).build())
                .header(AUTHORIZATION, accessToken)
                .build();

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

        if (response.isError()) {
            Type mapType = new TypeToken<HashMap<String, String>>() {}.getType();
            Map<String, String> responseBody = jsonToObject(response.getBody(), mapType);
            logger.error(responseBody.getOrDefault("Message", "unknown error"));
            throw logger.throwing(new Exception(format(
                    "Can not get information about the sensors. %s", response.getBody()
            )));
        }

        String bodyJson = response.getBody();
        List<String> errors = new ArrayList<>();
        if (!sensorInfoSchema.validateJsonArray(bodyJson, errors)) {
            logger.error("{} received sensor info json is not valid according to the schema.", bodyJson);
            logger.error(Arrays.toString(errors.toArray()));
            throw logger.throwing(new Exception(format(
                    "Received json data is not valid according to the schema. %s", bodyJson
            )));
        }

        logger.debug("Parsing body of the response from JSON format.");
        Type sensorInfoListType = new TypeToken<Collection<SensorInfo>>() {}.getType();
        List<SensorInfo> sensors = jsonToObject(bodyJson, sensorInfoListType);

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

        AllowedStation allowedStation = config.getAllowedStation();
        logger.debug("Creating an iterator over all received sensors.");
        for (SensorInfo sensor : sensors) {
            logger.debug("Loading a sensor {}.", sensor.getEui());
            if (sensor.isActive() && allowedStation.isAllowed(sensor.getEui())) {
                Long unitId = convertEuiToUnitId(sensor.getEui());
                if (unitId != null) {
                    logger.info("Saving an active sensor {}.", sensor.getEui());
                    sensorInfos.add(sensor);
                } else {
                    logger.warn("Sensor {} does not have an EUI converter.", sensor.getEui());
                }
            } else {
                logger.info("Sensor {} is not activated.", sensor.getEui());
            }
        }

        logger.info("{} sensors were loaded.", sensorInfos.size());
        logger.info(sensorInfos.toString());
    }

    @Override
    public AzureModel fetch(Optional<AzureSession> persistenceSession) {

        if (sensorInfos.isEmpty()) {
            logger.error("Sensors information were not loaded. Can not get detailed information.");
            OffsetDateTime now = OffsetDateTime.now();
            return new AzureModel(emptyList(), now, now);
        }

        AzureSession session = persistenceSession.filter(ProxySessionModel::isActive).orElse(localSession);

        String accessToken = authService.getAccessToken();

        int totalFetched = 0;
        OffsetDateTime globalFrom = MAX, globalTo = MIN;
        List<SensorInfo> usedSensors = new ArrayList<>(sensorInfos.size());
        for (SensorInfo sensorInfo : sensorInfos) {
            logger.info("Fetching data for the sensor {}.", sensorInfo.getEui());
            String stationId = sensorInfo.getEui();

            if (isBlank(stationId)) continue;

            ZoneOffset offset = sensorInfo.getSensorDataTime().getOffset();
            OffsetDateTime firstValueDate = OffsetDateTime.of(sensorInfo.getCreated(), offset);
            OffsetDateTime startDate = config.getStartDate().atOffset(offset);
            OffsetDateTime lastFetch = session.getLastTimeForUnit(stationId, MIN);

            long fromDateEpoch = Math.max(startDate.toEpochSecond(), Math.max(firstValueDate.toEpochSecond(), lastFetch.toEpochSecond()));
            OffsetDateTime localFrom = OffsetDateTime.ofInstant(Instant.ofEpochSecond(fromDateEpoch), offset);

            LocalDateTime endTime = config.getEndDate() != null ? config.getEndDate() : LocalDateTime.now();
            OffsetDateTime localTo = OffsetDateTime.of(endTime, localFrom.getOffset());

            HostConfig sensorDataHost = config.getSensorDataHost();
            logger.info("Creating a http request to {}.", sensorDataHost);
            HttpRequest request = HttpRequest.newBuilder()
                    .url(URLBuilder.newBuilder(sensorDataHost.getDomain(), sensorDataHost.getPath())
                            .addParam("eui", stationId)
                            .addParam("from", localFrom.format(ISO_DATE_TIME))
                            .addParam("to", localTo.format(ISO_DATE_TIME))
                            .addParam("limit", config.getLimitPerSensor())
                            .build())
                    .header(AUTHORIZATION, accessToken)
                    .GET().build();

            logger.info("Sending the http request.");
            logger.info(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());
                continue;
            }

            String bodyJson = response.getBody();
//            List<String> errors = new ArrayList<>();
//            if (!sensorDataSchema.validateJsonObject(bodyJson, errors)) {
//                logger.error("{} received sensor data json is not valid according to the schema.", bodyJson);
//                logger.error(Arrays.toString(errors.toArray()));
//                continue;
//            }

            SensorInfo sensorInfoData;
            int receivedRecords;
            try {
                logger.debug("Parsing body of the response.");
                sensorInfoData = jsonToObject(bodyJson, SensorInfo.class);
                receivedRecords = sensorInfoData.getData() == null ? 0 : sensorInfoData.getData().size();
                logger.info("Received {} records for the sensor {}.", receivedRecords, sensorInfo.getEui());
            } catch (SyntaxException e) {
                logger.catching(e);
                continue;
            }

            if (receivedRecords > 0 && sensorInfoData.getEui().equals(sensorInfo.getEui())) {
                logger.debug("Setting sensor data to the sensor {}.", sensorInfo.getEui());
                totalFetched += sensorInfoData.getData().size();

                logger.debug("Getting last record of the sensor data.");
                OffsetDateTime latestTimestamp = MIN;
                List<SensorData> sensorData = sensorInfoData.getData();
                if (!sensorData.isEmpty()) {
                    SensorData lastRecord = sensorData.get(sensorData.size() - 1);
                    OffsetDateTime lastRecordTime = lastRecord.getTime();
                    if (lastRecordTime.isAfter(latestTimestamp)) {
                        latestTimestamp = lastRecordTime;
                    }

                    usedSensors.add(new SensorInfo(sensorInfo, sensorData));
                }
                localTo = latestTimestamp;

                session.addLastUnitTime(stationId, localTo);

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

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


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

        if (totalFetched == 0 && config.getEndDate() != null) {
            throw new ModuleInterruptedException(format(
                    "Fetched all records from %s to %s. The connector is going to end.",
                    config.getStartDate().format(ISO_DATE_TIME), config.getEndDate().format(ISO_DATE_TIME)
            ));
        }

        logger.debug("Creating a new instance of Azure model.");
        AzureModel model = new AzureModel(usedSensors, globalFrom, globalTo);
        logger.debug("Model was created successfully and sending it forward.");

        return model;
    }
}
