// Copyright (c) 2020 UWB & LESP.
// The UWB & LESP license this file to you under the MIT license.

package cz.senslog.connector.fetch.senslog.v1;

import com.google.gson.reflect.TypeToken;
import cz.senslog.connector.fetch.api.ConnectorFetcher;
import cz.senslog.connector.model.v1.Record;
import cz.senslog.connector.tools.http.*;
import cz.senslog.connector.tools.util.Tuple;
import cz.senslog.connector.model.api.ProxySessionModel;
import cz.senslog.connector.model.v1.*;
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 java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

import static cz.senslog.connector.tools.json.BasicJson.jsonToObject;
import static java.time.OffsetDateTime.parse;
import static java.time.format.DateTimeFormatter.ofPattern;

public class SenslogFetcher implements ConnectorFetcher<SensLogSession, SenslogV1Model> {

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

    private static final int MAX_AUTH_ERRORS = 3;

    private final SenslogConfig config;
    private final HttpClient httpClient;

    private final SensLogSession localSession;
    private final Map<Long, UnitInfo> units;
    private Tuple<Boolean, HttpCookie> authCookie;
    private final AtomicInteger authErrors;


    SenslogFetcher(SenslogConfig config, HttpClient httpClient) {
        this.config = config;
        this.httpClient = httpClient;
        this.localSession = SensLogSession.emptySession();
        this.units = new HashMap<>();
        this.authCookie = Tuple.of(false, null);
        this.authErrors = new AtomicInteger(0);
    }

    SenslogFetcher() { this(null, null); }


    protected static class ObservationInfo { Float value; OffsetDateTime time; }

    private synchronized HttpCookie getAuthCookie() {
        /*
        if (authCookie.getItem1()) {
            return authCookie.getItem2();
        }

        if (config.getAuth() == null) {
            authCookie = Tuple.of(true, null);
            return authCookie.getItem2();
        }

         */

        HttpRequest request = HttpRequest.newBuilder().GET()
                .url(URLBuilder.newBuilder(config.getBaseUrl(), "/ControllerServlet")
                        .addParam("username", config.getAuth().getUsername())
                        .addParam("password", config.getAuth().getPassword())
                        .build()
                ).build();
        logger.info("Getting new auth cookie from the server: {}.", config.getBaseUrl());

        HttpResponse response = httpClient.send(request);
        logger.info("Received new auth token with the status '{}' from the server {}.", response.getStatus(), config.getBaseUrl());

        if (response.isError()) {
            logger.warn("Authorization failed. Error code {} with the reason '{}'.", response.getStatus(), response.getBody());
            HttpCookie cookie = HttpCookie.empty();
            authCookie = Tuple.of(false, cookie);
            return cookie;
        }

        final Type lastObsType = new TypeToken<Map<String, Object>>() {}.getType();
        Map<String, Object> jsonResponse = jsonToObject(response.getBody(), lastObsType);

        if (!jsonResponse.containsKey("sessionid")) {
            logger.error("Authorization failed. JSON does not contain session id. {}", response.getBody());
            HttpCookie cookie = HttpCookie.empty();
            authCookie = Tuple.of(false, cookie);
            return cookie;
        }

        String sessionId = (String) jsonResponse.get("sessionid");
        String domain = config.getBaseURI().getHost();
        String path = config.getBaseURI().getPath();
        HttpCookie cookie = new HttpCookie("JSESSIONID", sessionId, domain, path);
        authCookie = Tuple.of(true, cookie);
        return cookie;
    }

    private Optional<String> callRequest(HttpRequest.Builder reqBuilder) {
        HttpCookie authCookie = getAuthCookie();
        if (!authCookie.isSecure()) {
            logger.warn("Auth cookie is not valid to be used.");
            return Optional.empty();
        }

        HttpRequest request = reqBuilder.addCookie(authCookie).build();
        logger.info("Getting new data from the server: {}.", request.getUrl());

        HttpResponse response = httpClient.send(request);
        logger.info("Received new data with the status '{}' from the server {}.", response.getStatus(), request.getUrl());

        if (response.getStatus() == HttpCode.UNAUTHORIZED && authErrors.get() <= MAX_AUTH_ERRORS) {
            this.authCookie = Tuple.of(false, null);
            authErrors.incrementAndGet();
            return callRequest(reqBuilder);
        }

        if (response.isError()) {
            logger.error("code: {}, message: {}", response.getStatus(), response.getBody());
            throw new IllegalStateException(response.getBody());
        }

        authErrors.set(0);
        return Optional.of(response.getBody());
    }

    @Override
    public void init() throws Exception {

        HttpRequest.Builder reqUnitsBuilder = HttpRequest.newBuilder().GET()
                .url(URLBuilder.newBuilder(config.getBaseUrl(), "/DataService")
                        .addParam("Operation", "GetUnitsList")
                        .addParam("user", config.getUser())
                        .build());

        Optional<String> resUnitsBody = callRequest(reqUnitsBuilder);

        if (resUnitsBody.isPresent()) {
            Type unitInfoType = new TypeToken<Collection<UnitInfo>>() {}.getType();
            List<UnitInfo> unitInfos = jsonToObject(resUnitsBody.get(), unitInfoType);

            for (UnitInfo unit : unitInfos) {
                if (!config.getAllowedStation().isAllowed(unit.getUnitId())) {
                    logger.info("Unit {} is not allowed in configuration.", unit.getUnitId());continue;
                }

                units.put(unit.getUnitId(), unit);

                HttpRequest.Builder reqSensorsBuilder = HttpRequest.newBuilder().GET()
                        .url(URLBuilder.newBuilder(config.getBaseUrl(), "/SensorService")
                                .addParam("Operation", "GetSensors")
                                .addParam("user", config.getUser())
                                .addParam("unit_id", unit.getUnitId())
                                .build());

                Optional<String> resSensorsBody = callRequest(reqSensorsBuilder);

                if (resSensorsBody.isPresent()) {
                    Type sensorInfoType = new TypeToken<Collection<SensorInfo>>() {}.getType();
                    List<SensorInfo> sensors = jsonToObject(resSensorsBody.get(), sensorInfoType,
                            Tuple.of(OffsetDateTime.class, el -> parse(el + "00", ofPattern("yyyy-MM-dd HH:mm:ssZ"))));

                    logger.info("For the unit {} was added {} sensors.", unit.getUnitId(), sensors.size());
                    List<SensorInfo> allowedSensors = new ArrayList<>();
                    for (SensorInfo sensor : sensors) {
                        if (config.getAllowedStation().isAllowed(unit.getUnitId(), sensor.getSensorId())) {
                            allowedSensors.add(sensor);
                        }
                    }
                    unit.setSensors(allowedSensors);
                }
            }
        }
    }

    @Override
    public SenslogV1Model fetch(Optional<SensLogSession> persistenceSession) {
        HttpCookie authCookie = getAuthCookie();

        OffsetDateTime now = ZonedDateTime.ofInstant(Instant.now(), config.getTimeZone().toZoneId()).toOffsetDateTime();
        OffsetDateTime startDate = ZonedDateTime.of(config.getStartDate(), config.getTimeZone().toZoneId()).toOffsetDateTime();

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

        List<Record> records = new ArrayList<>();

        for (UnitInfo unit : units.values()) {
            for (SensorInfo sensor : unit.getSensors()) {

                OffsetDateTime firstValueDate = sensor.getFirstObservationTime();
                OffsetDateTime lastFetch = session.getLiveInfo(unit.getUnitId(), sensor.getSensorId()).lastFetch;

                OffsetDateTime fromDate = Stream.of(startDate, firstValueDate, lastFetch).max(Comparator.comparing(OffsetDateTime::toEpochSecond)).get();
                OffsetDateTime toDate = fromDate.plusMinutes(config.getInterval());

                if (fromDate.isBefore(now) && toDate.isAfter(now)) {
                    toDate = now;
                }

                logger.info("Getting new observations for the unit {} and the sensor {} from {} to {}.", unit.getUnitId(), sensor.getSensorId(), fromDate, toDate);

                final DateTimeFormatter pattern = ofPattern("yyyy-MM-dd HH:mm:ssZ");
                HttpRequest observationRequest = HttpRequest.newBuilder().GET()
                        .addCookie(authCookie)
                        .url(URLBuilder.newBuilder(config.getBaseUrl(), "/SensorService")
                                .addParam("Operation", "GetObservations")
                                .addParam("user", config.getUser())
                                .addParam("unit_id", unit.getUnitId())
                                .addParam("sensor_id", sensor.getSensorId())
                                .addParam("from", fromDate.format(pattern))
                                .addParam("to", toDate.format(pattern))
                                .build())
                        .build();

                HttpResponse observationResponse = httpClient.send(observationRequest);

                if (observationResponse.isOk()) {
                    Type observationType = new TypeToken<Collection<ObservationInfo>>() {}.getType();
                    List<ObservationInfo> observations = jsonToObject(observationResponse.getBody(), observationType,
                            Tuple.of(OffsetDateTime.class, el -> parse(el + "00", ofPattern("yyyy-MM-dd HH:mm:ssZ")))
                    );

                    OffsetDateTime lastTimeStamp = toDate;
                    for (ObservationInfo observation : observations) {
                        Observation obs = new Observation();
                        obs.setSensorId(sensor.getSensorId());
                        obs.setUnitId(unit.getUnitId());
                        obs.setValue(observation.value);
                        obs.setTime(observation.time.toZonedDateTime().toOffsetDateTime());

                        records.add(obs);

                        if (observation.time.isAfter(lastTimeStamp)) {
                            lastTimeStamp = observation.time;
                        }
                    }

                    session.updateLastFetch(unit.getUnitId(), sensor.getSensorId(), lastTimeStamp);
                } else {
                    logger.error("Request railed: {}", observationResponse.getBody());
                }
            }
        }

        return new SenslogV1Model(units, records, session.getGlobalFrom(), session.getGlobalTo());
    }
}
